diff --git a/.gitignore b/.gitignore index 21ae973..c5c121d 100644 --- a/.gitignore +++ b/.gitignore @@ -36,5 +36,4 @@ ref/ .factorypath **/bin/ .claude/ -docs/ .ruff_cache/ diff --git a/TODO.md b/TODO.md index 8f5d03d..be3ab0d 100644 --- a/TODO.md +++ b/TODO.md @@ -1,12 +1,21 @@ # LoVisual -Проект состоит из одного компонента — мода. +Проект: клиентский мод (`mod/`, бывш. `combatant-client-26.2/`) + платформа +(`backend/`, `frontend/` — сайт+бэкенд), см. Фазу 10/Подсистемы 1-3 ниже. -| Компонент | Стек | TODO | -|-----------|------|------| -| `combatant-client-26.2/` | Java 25, Fabric (MC 26.2) | [`combatant-client-26.2/TODO.md`](combatant-client-26.2/TODO.md) | +> Старый лаунчер (Rust + Slint) и старый backend (Rust + axum, другой дизайн) +> были удалены раньше (решение 2026-09-05) — `backend/` и `frontend/` сейчас +> строятся заново по дизайну из Фазы 10 / Подсистем 1-3, это не восстановление +> старого кода. -> Лаунчер (Rust + Slint) и backend (Rust + axum) удалены из проекта — развитие только мода. +> **Коммиты — на английском** (решение 2026-09-23, действует с этого момента +> вперёд; более старые коммиты на русском не переписываем). + +> **TEMP-NOTE (удалить этот блок, когда сделано):** после окончания текущего +> рефакторинга владелец squash'ит историю git с 100+ коммитов до ~20, затем +> пушит и открывает репозиторий из приватного в публичный (сейчас приват — +> ранний код был спижен). Это план владельца, не техническая задача агента — +> просто держать коммиты чистыми/по делу до этого момента. --- @@ -20,6 +29,58 @@ --- +## Правила платформы (`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 @@ -27,15 +88,18 @@ - [x] Фаза 3.5: Удаление читерских модулей (легит-направление) - [x] Фаза 4: Security (BackdoorProtection, StaffTracker) - [x] Фаза 5: Нормализация модулей (ленивый Minecraft, GameClock) -- [x] Фаза 6: Тесты (EventBus, конфиг, утилиты) — см. `combatant-client-26.2/TODO.md` +- [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), - план в `combatant-client-26.2/TODO.md` + план в `mod/TODO.md` - [ ] Фаза 9: Оптимизация FPS — 9.1 (мёртвые грузы) ЗАВЕРШЕНА 2026-09-11 (137121e…4d60d3d); 9.2 Optimize гейтится Фазой 8.5, 9.3 A/B-мерка needs запуск на Windows +- [ ] Фаза 10: Платформа (сайт + бэкенд) — Подсистемы 1/2/3 спроектированы 2026-09-23 + (см. ниже). Реализация: план backend Подсистемы 1 в `backend/PLAN.md` + (написан 2026-09-23), frontend/Подсистема 2/3 — планы позже. --- @@ -121,3 +185,266 @@ - [x] **Фикс ChinaHat** — текущая геометрия кривая (смахивает на самбреро/зонт); переделать модель в нормальную конусную шляпу по референсу Soup (`visuals/ChinaHat.java` + `ChinaHatModel.java`). + +--- + +## Фаза 10: Платформа — сайт + бэкенд (дизайн от 2026-09-23) + +> Прим.: лаунчер и старый backend (Rust+axum) были удалены ранее (см. шапку файла) — +> это НЕ восстановление старого кода, а новый дизайн с нуля под текущие требования. +> Статус: дизайн готов, реализация backend начинается — план в `backend/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 (не перебираем) + +## Подсистема 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 сообщения с уведомлением клиенту, без разрыва соединения. diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..1b3380f --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,7 @@ +DATABASE_URL=postgres://lovisual:lovisual@localhost:5432/accounts_db +JWT_SECRET=change-me-to-a-long-random-string +PORT=8081 +S3_ENDPOINT=http://localhost:9000 +S3_BUCKET=lovisual-avatars +S3_ACCESS_KEY=minioadmin +S3_SECRET_KEY=minioadmin diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..fedaa2b --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,2 @@ +/target +.env diff --git a/backend/Cargo.lock b/backend/Cargo.lock new file mode 100644 index 0000000..9ec7e67 --- /dev/null +++ b/backend/Cargo.lock @@ -0,0 +1,3931 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "accounts-service" +version = "0.1.0" +dependencies = [ + "anyhow", + "argon2", + "aws-config", + "aws-sdk-s3", + "axum", + "axum-test", + "chrono", + "dashmap", + "dotenvy", + "jsonwebtoken", + "rand 0.10.3", + "serde", + "serde_json", + "sqlx", + "tokio", + "tower-http", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arc-swap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "argon2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134c52ddac6d63c576bef8168db10c83c49c26444ecbc68060fef078925a901c" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures 0.3.1", + "password-hash", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "aws-config" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8d7b388a9fc3a6db15a5ec778c38b354eff1364882c94d08e0252f7a47dcaa4" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sdk-sso", + "aws-sdk-ssooidc", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "hex", + "http 1.5.0", + "sha1 0.10.7", + "time", + "tokio", + "tracing", + "url", + "zeroize", +] + +[[package]] +name = "aws-credential-types" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93964ffdaf57857f544be3666a5f57570bb699e934700f11b49708f61bb556e" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "zeroize", +] + +[[package]] +name = "aws-lc-rs" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "aws-runtime" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b8a9911551b4ea6ca13805ef52ed96f7d2bbb43cc3b4a14cb0776a71f33cfaa" +dependencies = [ + "aws-credential-types", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "bytes-utils", + "fastrand", + "http 1.5.0", + "http-body 1.1.0", + "percent-encoding", + "pin-project-lite", + "tracing", + "uuid", +] + +[[package]] +name = "aws-sdk-s3" +version = "1.149.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d48257fb0eb1907a02a965ff7d1b910770a6de2c59a2d03461678df989c5cdcd" +dependencies = [ + "arc-swap", + "aws-credential-types", + "aws-runtime", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-checksums", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "bytes", + "fastrand", + "hex", + "hmac 0.13.0", + "http 1.5.0", + "http-body 1.1.0", + "lru", + "percent-encoding", + "regex-lite", + "sha2 0.11.0", + "tracing", + "url", +] + +[[package]] +name = "aws-sdk-sso" +version = "1.112.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d59c47556b283bb2d20fc0b416df7d9d9a3d6f5fa0e77867167a3f2efc23b9" +dependencies = [ + "arc-swap", + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 1.5.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-ssooidc" +version = "1.114.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eceb9f31b46ba6da21f666e456395624824a2a086caed940b13d78832fa3ee02" +dependencies = [ + "arc-swap", + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 1.5.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sts" +version = "1.117.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9291df9ea767ab5e77d08a53f4f22daa917909a34ed7a22c1a4b82d193a423ad" +dependencies = [ + "arc-swap", + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "fastrand", + "http 1.5.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sigv4" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2312577f088c9fbf4206dfdb884cf1de9407b43e1a923cbed5237775116fc24b" +dependencies = [ + "aws-credential-types", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "crypto-bigint", + "form_urlencoded", + "hex", + "hmac 0.13.0", + "http 1.5.0", + "p256", + "percent-encoding", + "sha2 0.11.0", + "subtle", + "time", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-async" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02e407fb3b54891734224b9ffac8a71fdd35f542500fa1af95754a6b2beb316" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "aws-smithy-checksums" +version = "0.65.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67ecd999972b58e67cab052f5129906c08c25883bd0788ceefc55ef97d61307" +dependencies = [ + "aws-smithy-http", + "aws-smithy-types", + "bytes", + "crc-fast", + "hex", + "http 1.5.0", + "http-body 1.1.0", + "http-body-util", + "md-5", + "pin-project-lite", + "sha1 0.11.0", + "sha2 0.11.0", + "tracing", +] + +[[package]] +name = "aws-smithy-eventstream" +version = "0.61.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80c2051c2f1016fb8e6548dd07b8bc2ac9c3fe583721444b92f515e856d31609" +dependencies = [ + "aws-smithy-types", + "bytes", + "crc32fast", +] + +[[package]] +name = "aws-smithy-http" +version = "0.64.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37843d9add67c3aff5856f409c6dc315d3cdff60f9c0cb5b670dab1e9920306d" +dependencies = [ + "aws-smithy-eventstream", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "futures-util", + "http 1.5.0", + "http-body 1.1.0", + "http-body-util", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-http-client" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bd25384a4e437aa8d8f339afad4b69e786b936a7cb10db668a7aaf66717b1a8" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "h2 0.3.27", + "h2 0.4.19", + "http 0.2.12", + "http 1.5.0", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper 1.11.1", + "hyper-rustls 0.24.2", + "hyper-rustls 0.27.10", + "hyper-util", + "pin-project-lite", + "rustls 0.21.12", + "rustls 0.23.45", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.5", + "tower", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.63.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3385d469edbe8b60cc72002784652b5efca39178192aa9cc4b44c9875c6bdc18" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-observability" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e86338c869539a581bf161247762a6e87f92c5c075060057b5ed6d06632ed0c" +dependencies = [ + "aws-smithy-runtime-api", +] + +[[package]] +name = "aws-smithy-query" +version = "0.62.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1d1d71f6562be974caa85442ecd90194c40fdb5df045f182a6c2e872ce95056" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "aws-smithy-xml", + "urlencoding", +] + +[[package]] +name = "aws-smithy-runtime" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "591e0024cdf2a8de8711860f7e1d5cfbca26b800ce13793a9aa2f4f5918e6d95" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-http-client", + "aws-smithy-observability", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "bytes", + "fastrand", + "http 1.5.0", + "http-body 1.1.0", + "http-body-util", + "pin-project-lite", + "pin-utils", + "tokio", + "tracing", +] + +[[package]] +name = "aws-smithy-runtime-api" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf162725183ec0df77a9dc82ce22d9ba0b6ea7f376e8ec4f6b695727c70698" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api-macros", + "aws-smithy-types", + "bytes", + "http 0.2.12", + "http 1.5.0", + "pin-project-lite", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-runtime-api-macros" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "221eaa237ddf1ca79b60d1372aad77e47f9c0ea5b3ce5099da8c61d027dc77b3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "aws-smithy-schema" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8f395d93304280b64b7632fea798d177e74897fe7f063416ce627cd6fa24829" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "http 1.5.0", +] + +[[package]] +name = "aws-smithy-types" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16ecf7989e381d0cbb7ca06104140d940bf38d8eb9004bd3a12d23df03e3d363" +dependencies = [ + "base64-simd", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http 1.5.0", + "http-body 0.4.6", + "http-body 1.1.0", + "http-body-util", + "itoa", + "num-integer", + "pin-project-lite", + "pin-utils", + "ryu", + "serde", + "time", + "tokio", + "tokio-util", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.62.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b932c8d6dc127fc980eecd78f8694ae9b9551b69a93a7def2a199c1c0033daf" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "209f3a6d82a6e9e5f94abbed94c7a26e1c052341002bf57a5fb5481f625896fc" +dependencies = [ + "aws-credential-types", + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "rustc_version", + "tracing", +] + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http 1.5.0", + "http-body 1.1.0", + "http-body-util", + "hyper 1.11.1", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "multer", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http 1.5.0", + "http-body 1.1.0", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-test" +version = "21.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bf0893561659ea1365921a0aed8bbb620fae83dae00b53fe64d41fea4877640" +dependencies = [ + "anyhow", + "axum", + "bytes", + "bytesize", + "cookie", + "educe", + "expect-json", + "http 1.5.0", + "http-body-util", + "hyper 1.11.1", + "hyper-util", + "mime", + "pretty_assertions", + "reserve-port", + "rust-multipart-rfc7578_2", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tower", + "url", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" +dependencies = [ + "serde_core", +] + +[[package]] +name = "blake2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b5d4d889834ee8ecfc0f8426ad30faf7cdcb10f741a8e6d7224d95325479f6f" +dependencies = [ + "digest 0.11.3", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "bytes-utils" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" +dependencies = [ + "bytes", + "either", +] + +[[package]] +name = "bytesize" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7354288c522e7e980fafd2075d63d1285794c3a6a16cdd492f189ea406e5f18b" + +[[package]] +name = "cc" +version = "1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54413ede23c2daf518f35156dfde027feb2374004d63bd497f983c8db9c0e313" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "cookie" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + +[[package]] +name = "crc-fast" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e75b2483e97a5a7da73ac68a05b629f9c53cff58d8ed1c77866079e18b00dba5" +dependencies = [ + "digest 0.10.7", + "spin 0.10.1", +] + +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e8bd762f7479489c70ed6c768ddca99d7296857de437a68dcb2a94365b3fae" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dashmap" +version = "6.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid 0.9.6", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.6", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid 0.10.2", + "crypto-common 0.2.2", + "ctutils", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2 0.10.9", + "subtle", + "zeroize", +] + +[[package]] +name = "educe" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e451fac8dd8dece16234604bf1efce6e90fddd8ab6ad4d66eec0eca5160959dd" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" +dependencies = [ + "serde", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "hkdf 0.12.4", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +dependencies = [ + "serde", +] + +[[package]] +name = "encoding_rs" +version = "0.8.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5ef0006ac9ab233c38522f5ae99cae3625151de8f706cacee1cba4b8e2832a" +dependencies = [ + "cfg-if", + "core_detect", + "multiversion", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", +] + +[[package]] +name = "enum-ordinalize" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89dd01549b09589510cf0647475075d12071456586d70f5c75c98ae2a5537677" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65863d15a4ce2888bd2f0f543cc963d3879c3a022c8ee43f6141d479a3ac815" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etcetera" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" +dependencies = [ + "cfg-if", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "expect-json" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e80819dbfe83c8a651f5344b08910d0037dac72988aef27ee4e6bedd7ae2e33" +dependencies = [ + "chrono", + "email_address", + "expect-json-macros", + "num", + "regex", + "serde", + "serde_json", + "thiserror", + "typetag", + "uuid", +] + +[[package]] +name = "expect-json-macros" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0637949cd816934f3b7aab44ff98e7ec1fb903c379e07dcb9eac943ec33499e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b" + +[[package]] +name = "flume" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +dependencies = [ + "futures-core", + "futures-sink", + "spin 0.9.9", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "rand_core 0.10.1", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.5.0", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac 0.12.1", +] + +[[package]] +name = "hkdf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" +dependencies = [ + "hmac 0.13.0", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http 1.5.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http 1.5.0", + "http-body 1.1.0", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2 0.4.19", + "http 1.5.0", + "http-body 1.1.0", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.32", + "log", + "rustls 0.21.12", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53" +dependencies = [ + "http 1.5.0", + "hyper 1.11.1", + "hyper-util", + "rustls 0.23.45", + "rustls-native-certs", + "tokio", + "tokio-rustls 0.26.5", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http 1.5.0", + "http-body 1.1.0", + "hyper 1.11.1", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.5", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "inventory" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "11.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e75fe14a82d81e5f5af639997db37d8b96045938a7ac6ab18cdbe1c7467e05e1" +dependencies = [ + "base64", + "ed25519-dalek", + "getrandom 0.2.17", + "hmac 0.12.1", + "js-sys", + "p256", + "p384", + "rand 0.8.8", + "rsa", + "serde", + "serde_json", + "sha2 0.10.9", + "signature", + "zeroize", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin 0.9.9", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libsqlite3-sys" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "lru" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef9ac18847474e638e3702b76c65d4eb93428471a74778ef0f1be711717f89b5" +dependencies = [ + "hashbrown 0.17.1", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "md-5" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest 0.11.3", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http 1.5.0", + "httparse", + "memchr", + "mime", + "spin 0.9.9", + "version_check", +] + +[[package]] +name = "multiversion" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ca4bea16ffc3f443cf7d866912118196bfef4c6a1556ca00f9f9b00bb43f7c" +dependencies = [ + "multiversion-macros", +] + +[[package]] +name = "multiversion-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d416831a7317ef4b08bee00b69cbbb9c8763da7959a7026244d6266869f9c83" +dependencies = [ + "proc-macro2", + "quote", + "rustversion", + "syn 3.0.6", +] + +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.8", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "password-hash" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aab41826031698d6ffcd9cff78ef56ef998e39dc7e5067cdfebe373842d4723b" +dependencies = [ + "getrandom 0.4.3", + "phc", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phc" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44dc769b75f93afdddd8c7fa12d685292ddeff1e66f7f0f3a234cf1818afe892" +dependencies = [ + "base64ct", + "ctutils", + "getrandom 0.4.3", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reserve-port" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71ea98a177596a4579881992bd2bd4af27772fc95d0e5f5668a8f9535eca6380" +dependencies = [ + "thiserror", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac 0.12.1", + "subtle", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid 0.9.6", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rust-multipart-rfc7578_2" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00bdaa068902270ca7fa8619775e1838e23a63620abac0947ce0f715819b8cec" +dependencies = [ + "bytes", + "futures-core", + "futures-util", + "http 1.5.0", + "mime", + "rand 0.10.3", + "thiserror", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + +[[package]] +name = "rustls" +version = "0.23.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +dependencies = [ + "aws-lc-rs", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki 0.103.15", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "digest 0.11.3", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "digest 0.11.3", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spin" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "378620ccc25c62c89d8be1c819e76a88d59bdcc3304733330788948e619bfd71" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b44e85bf579a8eeb4ceaa77a3a523baf2bf0e9bac7e40f405d537b5d2d5ccb" +dependencies = [ + "base64", + "bytes", + "cfg-if", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.16.1", + "hashlink", + "indexmap", + "log", + "memchr", + "percent-encoding", + "rustls 0.23.45", + "serde", + "serde_json", + "sha2 0.10.9", + "smallvec", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", + "webpki-roots", +] + +[[package]] +name = "sqlx-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd2b84f2bc39a5705ef27ec785a11c934a41bbd4a24941e257927cddc26b60bf" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.119", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb8d96de5fdc85a5c4ec813432b523ec637e80ba98f046555f75f7908ddac7c3" +dependencies = [ + "cfg-if", + "dotenvy", + "either", + "heck", + "hex", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.9", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.119", + "thiserror", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90b8020fe17c5f2c245bfa2505d7ef59c5604839527c740266ad2214acebea27" +dependencies = [ + "bitflags", + "byteorder", + "bytes", + "chrono", + "crc", + "digest 0.11.3", + "dotenvy", + "either", + "futures-core", + "futures-util", + "generic-array", + "log", + "percent-encoding", + "serde", + "sha1 0.11.0", + "sha2 0.11.0", + "sqlx-core", + "thiserror", + "tracing", + "uuid", +] + +[[package]] +name = "sqlx-postgres" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87a2bdd6e83f6b3ea525ca9fee568030508b58355a43d0b2c1674d5f79dcd65e" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf 0.13.0", + "hmac 0.13.0", + "itoa", + "log", + "md-5", + "memchr", + "rand 0.10.3", + "serde", + "serde_json", + "sha2 0.11.0", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488e99c397a62007e4229aec669a179816339afc6d2620ca6fa420dbee2e982c" +dependencies = [ + "atoi", + "chrono", + "flume", + "form_urlencoded", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "thiserror", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.5", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" +dependencies = [ + "rustls 0.23.45", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08a05a66a4fdd61cbbe0a1d755ffe0ca6aba159dd4820936a0ff8a8278245b9c" +dependencies = [ + "bitflags", + "bytes", + "http 1.5.0", + "http-body 1.1.0", + "percent-encoding", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "typetag" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c90e86058a30d42a1a928dfb4b49bb33c98c3a2b4909492e6b0881cd94798ec2" +dependencies = [ + "erased-serde", + "inventory", + "once_cell", + "serde", + "typetag-impl", +] + +[[package]] +name = "typetag-impl" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f153acc4e99a5f2a5aefa09fb078be54e26271b2813f6041200b224c098d8328" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.6", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626c4bac6755d76ffc12cb01b2eac751db1996b9e0041de9aa02c8c211ddc82c" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d35102a9f36d089ccae9e4c6802bc118be4487b80aaffc0ab4e0cf5ce92d2873" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c01f5ab44258da43cf276c74a2763db2ff3969c9c652c3f2de07041d0b2bc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/backend/Cargo.toml b/backend/Cargo.toml new file mode 100644 index 0000000..87c7191 --- /dev/null +++ b/backend/Cargo.toml @@ -0,0 +1,12 @@ +[workspace] +resolver = "2" +members = [ + "accounts-service", +] + +# Planned members, added when their own implementation plan starts +# (see backend/STRUCTURE.md and TODO.md Фаза 10 / Подсистемы 1-3): +# "gateway" — API gateway: routing + single JWT check point + rate limits +# "configs-service" — 4 config slots, share codes, showcase (Подсистема 1) +# "chat-service" — RPC chat, friends, presence, telemetry widgets (Подсистема 2) +# "addons-registry" — addon marketplace backend (Подсистема 3) diff --git a/backend/PLAN.md b/backend/PLAN.md new file mode 100644 index 0000000..0447708 --- /dev/null +++ b/backend/PLAN.md @@ -0,0 +1,1744 @@ +# Backend: accounts-service (Подсистема 1, часть 1) — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Standalone `accounts-service` (Rust/Axum) with email+password auth, JWT issue/refresh, OAuth-2-style device-code linking for the mod, and avatar upload — the foundation the gateway, configs-service and the site will build on. + +**Architecture:** Single Axum binary, `sqlx` against PostgreSQL (own `accounts_db`), `argon2` for password hashing, `jsonwebtoken` for access/refresh tokens, avatars stored in S3-compatible storage via the `aws-sdk-s3` crate (MinIO-compatible). No gateway yet — this service is directly reachable on its own port for this plan; the gateway that fronts it (routing + JWT check for the site/mod) is a **separate follow-up plan**, as is `configs-service` and the React frontend. This keeps each plan independently testable, per the microservices decomposition agreed in `/storage/project/jvm/LoVisual/TODO.md` (Фаза 10). + +**Tech Stack:** Rust (stable, edition 2024), Axum 0.8, sqlx 0.9 (postgres, runtime-tokio, tls-rustls, macros, migrate), tokio, argon2 0.6, jsonwebtoken 11 (rust_crypto backend), aws-sdk-s3, serde/serde_json, uuid (v4), rand 0.10 (for device/share codes), tower-http 0.7 (trace, cors), dotenvy. All versions verified by compiling on 2026-09-23 — see the note after Task 1 Step 1 for exact API deltas from older docs/tutorials. + +## Global Constraints + +- All money/user-facing strings: Russian (matches project convention seen in `TODO.md`/`mod/`), but code identifiers/comments in English (matches existing mod codebase style). +- No panics on user input — every handler returns a typed error mapped to an HTTP status, never `.unwrap()` on request data. +- Passwords: argon2id, never logged, never returned in any response. +- Every new module gets tests before being wired into `main.rs` (TDD, per project convention already established in `mod/`: "Тесты обязательны для новых модулей"). +- Max 250 lines per file (facades/delegates excepted) — see `TODO.md`'s "Правила платформы" section; every file created by this plan stays under that on its own, but keep it in mind if a later task grows one. +- Max 4 files per directory (subdirectories don't count against this) — same rule as the mod, see `TODO.md`. Every module directory in this plan (`auth/`, `accounts/`, `device/`, `avatars/`) stays at 3-4 files; if a later task needs a 5th file in one, split a meaningfully-named subdirectory instead of just adding the file. +- No `.clone()` to route around the borrow checker — take a reference instead; no blocking I/O inside an `async fn`; no God-structs (state is split per domain: `AuthState`, `DeviceState`, `AvatarState`, never one `AppState` holding everything) — see `TODO.md`'s anti-pattern list. +- Commit after every passing task, in this repo (`/storage/project/jvm/LoVisual`), path `backend/`. + +--- + +## File Structure + +``` +backend/ + accounts-service/ + Cargo.toml + migrations/ + 0001_init.sql + src/ + main.rs # axum app assembly, router, DB pool, S3 client + config.rs # env var loading (DATABASE_URL, JWT_SECRET, S3_*, PORT) + error.rs # AppError enum -> IntoResponse + db.rs # PgPool type alias + connect() + auth/ + mod.rs # re-exports + password.rs # hash_password, verify_password (argon2) + jwt.rs # issue_access_token, issue_refresh_token, verify_access_token + handlers.rs # POST /auth/register, /auth/login, /auth/refresh, /auth/logout + device/ + mod.rs + handlers.rs # POST /device/code, /device/confirm, /device/token + store.rs # in-memory device-code store (DashMap) with TTL + avatars/ + mod.rs + handlers.rs # POST /avatars, multipart upload + storage.rs # S3 put_object wrapper + accounts/ + mod.rs + model.rs # Account struct, row mapping + repo.rs # sqlx queries: create, find_by_email, find_by_id, update_avatar_url +``` + +--- + +### Task 1: Project scaffold + health check + +**Files:** +- Create: `backend/accounts-service/Cargo.toml` +- Create: `backend/accounts-service/src/main.rs` +- Create: `backend/accounts-service/src/config.rs` +- Create: `backend/.env.example` +- Test: `backend/accounts-service/tests/health.rs` + +**Interfaces:** +- Produces: `fn build_app(pool: sqlx::PgPool) -> axum::Router` (used by every later task to mount new routes; tests call this directly instead of spawning a real server) +- Produces: `struct Config { database_url: String, jwt_secret: String, port: u16, s3_endpoint: String, s3_bucket: String, s3_access_key: String, s3_secret_key: String }` with `Config::from_env() -> anyhow::Result` + +- [ ] **Step 1: Create the crate** + +```bash +mkdir -p /storage/project/jvm/LoVisual/backend/accounts-service/src +cd /storage/project/jvm/LoVisual/backend/accounts-service +cat > Cargo.toml << 'EOF' +[package] +name = "accounts-service" +version = "0.1.0" +edition = "2024" + +[dependencies] +axum = { version = "0.8", features = ["multipart"] } +tokio = { version = "1", features = ["rt-multi-thread", "macros"] } +tower-http = { version = "0.7", features = ["trace", "cors"] } +tracing = "0.1" +tracing-subscriber = "0.3" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +sqlx = { version = "0.9", default-features = false, features = ["runtime-tokio", "tls-rustls", "postgres", "uuid", "chrono", "macros", "migrate"] } +uuid = { version = "1", features = ["v4", "serde"] } +chrono = { version = "0.4", features = ["serde"] } +argon2 = "0.6" +jsonwebtoken = { version = "11", default-features = false, features = ["rust_crypto"] } +rand = "0.10" +dashmap = "6" +aws-sdk-s3 = "1" +aws-config = "1" +anyhow = "1" +dotenvy = "0.15" + +[dev-dependencies] +axum-test = "21" +EOF +``` + +> Versions verified by actually compiling each one (not from memory) on +> 2026-09-23 against Rust 1.97.1 — several had breaking changes since the +> versions this plan originally targeted, called out inline where the code +> differs from what you'd expect from older docs/tutorials: +> - **sqlx 0.8 → 0.9**: the combined `runtime-tokio-rustls` feature was +> split into separate `runtime-tokio` + `tls-rustls` features (hence +> `default-features = false` above — the new defaults pull in `any`/mysql/ +> sqlite machinery this service doesn't need). `FromRow`/`query_as`/ +> `PgPoolOptions` themselves are unchanged. +> - **argon2 0.5 → 0.6**: `SaltString`/`PasswordHash` moved and the manual +> two-argument `hash_password(bytes, &salt)` call is gone — see Task 3, +> the salt is now generated internally by `hash_password(bytes)` alone. +> - **jsonwebtoken 9 → 11**: now requires explicitly picking a crypto +> backend feature (`rust_crypto` here — pure Rust, no OpenSSL/BoringSSL +> toolchain needed) or it panics at runtime with "Could not automatically +> determine the process-level CryptoProvider". `encode`/`decode` calls +> themselves are unchanged. +> - **rand 0.8 → 0.10**: `rand::thread_rng()` → `rand::rng()`, and +> `Rng::gen_range` moved to the `RngExt` trait as `random_range` — see +> Task 7's `random_user_code()`. +> - **axum-test 16 → 21**: `TestServer::new(app)` now returns `Self` +> directly instead of `Result` — drop the `.unwrap()` every test +> in this plan that constructs one. +> - **`thiserror` dropped** from the dependency list — nothing in this plan +> actually derives `thiserror::Error` (`AppError` in Task 6 is hand-written), +> so it was dead weight; add it back only if a later task needs it. + +- [ ] **Step 2: Write `config.rs`** + +```rust +// backend/accounts-service/src/config.rs +use anyhow::{Context, Result}; + +#[derive(Clone)] +pub struct Config { + pub database_url: String, + pub jwt_secret: String, + pub port: u16, + pub s3_endpoint: String, + pub s3_bucket: String, + pub s3_access_key: String, + pub s3_secret_key: String, +} + +impl Config { + pub fn from_env() -> Result { + Ok(Config { + database_url: std::env::var("DATABASE_URL") + .context("DATABASE_URL not set")?, + jwt_secret: std::env::var("JWT_SECRET") + .context("JWT_SECRET not set")?, + port: std::env::var("PORT") + .unwrap_or_else(|_| "8081".into()) + .parse() + .context("PORT must be a number")?, + s3_endpoint: std::env::var("S3_ENDPOINT") + .context("S3_ENDPOINT not set")?, + s3_bucket: std::env::var("S3_BUCKET") + .context("S3_BUCKET not set")?, + s3_access_key: std::env::var("S3_ACCESS_KEY") + .context("S3_ACCESS_KEY not set")?, + s3_secret_key: std::env::var("S3_SECRET_KEY") + .context("S3_SECRET_KEY not set")?, + }) + } +} +``` + +- [ ] **Step 3: Write `main.rs` with a health route** + +```rust +// backend/accounts-service/src/main.rs +mod config; + +use axum::{routing::get, Router}; + +pub fn build_app(_pool: sqlx::PgPool) -> Router { + Router::new().route("/health", get(|| async { "ok" })) +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt::init(); + dotenvy::dotenv().ok(); + let cfg = config::Config::from_env()?; + + let pool = sqlx::postgres::PgPoolOptions::new() + .max_connections(10) + .connect(&cfg.database_url) + .await?; + + let app = build_app(pool); + let listener = tokio::net::TcpListener::bind(("0.0.0.0", cfg.port)).await?; + tracing::info!("accounts-service listening on {}", cfg.port); + axum::serve(listener, app).await?; + Ok(()) +} +``` + +- [ ] **Step 4: Create `backend/.env.example`** + +```bash +cat > /storage/project/jvm/LoVisual/backend/.env.example << 'EOF' +DATABASE_URL=postgres://lovisual:lovisual@localhost:5432/accounts_db +JWT_SECRET=change-me-to-a-long-random-string +PORT=8081 +S3_ENDPOINT=http://localhost:9000 +S3_BUCKET=lovisual-avatars +S3_ACCESS_KEY=minioadmin +S3_SECRET_KEY=minioadmin +EOF +``` + +- [ ] **Step 5: Write the failing test** + +```rust +// backend/accounts-service/tests/health.rs +// Requires `pub mod` exposure — add `pub mod config;` and make build_app pub +// in a lib target. Add to Cargo.toml under [package]: +// [lib] +// name = "accounts_service" +// path = "src/lib.rs" +// and move the `build_app` fn (and `mod config;`) into src/lib.rs, with +// main.rs reduced to `use accounts_service::{build_app, config};` + the +// existing #[tokio::main] body unchanged. + +use axum_test::TestServer; + +#[tokio::test] +async fn health_returns_ok() { + // NOTE: this test does not touch the DB — pass a pool that is never + // queried. sqlx::PgPool::connect_lazy never opens a connection until + // a query runs, so this is safe without a running Postgres. + let pool = sqlx::PgPool::connect_lazy("postgres://user:pass@localhost/db") + .expect("lazy pool"); + let app = accounts_service::build_app(pool); + let server = TestServer::new(app); + + let response = server.get("/health").await; + response.assert_status_ok(); + response.assert_text("ok"); +} +``` + +- [ ] **Step 6: Move `build_app`/`mod config` into `src/lib.rs` as described in the test's NOTE** + +```rust +// backend/accounts-service/src/lib.rs +pub mod config; + +use axum::{routing::get, Router}; + +pub fn build_app(_pool: sqlx::PgPool) -> Router { + Router::new().route("/health", get(|| async { "ok" })) +} +``` + +```rust +// backend/accounts-service/src/main.rs +use accounts_service::{build_app, config::Config}; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt::init(); + dotenvy::dotenv().ok(); + let cfg = Config::from_env()?; + + let pool = sqlx::postgres::PgPoolOptions::new() + .max_connections(10) + .connect(&cfg.database_url) + .await?; + + let app = build_app(pool); + let listener = tokio::net::TcpListener::bind(("0.0.0.0", cfg.port)).await?; + tracing::info!("accounts-service listening on {}", cfg.port); + axum::serve(listener, app).await?; + Ok(()) +} +``` + +Add to `Cargo.toml` (after `[package]`, before `[dependencies]`): + +```toml +[lib] +name = "accounts_service" +path = "src/lib.rs" +``` + +- [ ] **Step 7: Run the test** + +Run: `cd /storage/project/jvm/LoVisual/backend/accounts-service && cargo test --test health` +Expected: `test health_returns_ok ... ok` + +- [ ] **Step 8: Commit** + +```bash +cd /storage/project/jvm/LoVisual +git add backend/accounts-service backend/.env.example +git commit -m "feat(backend): scaffold accounts-service with health check" +``` + +--- + +### Task 2: Database schema — `accounts` table + migration runner + +**Files:** +- Create: `backend/accounts-service/migrations/0001_init.sql` +- Modify: `backend/accounts-service/src/lib.rs` (run migrations on startup is done in `main.rs`, not `build_app` — tests don't need a live DB for Task 1's test) +- Modify: `backend/accounts-service/Cargo.toml` (sqlx already has `migrate` via the `sqlx` feature `migrate`; add it) +- Test: `backend/accounts-service/tests/db_migration.rs` + +**Interfaces:** +- Produces: `accounts` table with columns `id uuid PK, email text UNIQUE, password_hash text, display_nick text, role text NOT NULL DEFAULT 'user', can_publish_addons boolean NOT NULL DEFAULT true, created_at timestamptz NOT NULL DEFAULT now()` +- Produces: `avatars` table `account_id uuid PK REFERENCES accounts(id), s3_key text, uploaded_at timestamptz NOT NULL DEFAULT now()` +- Produces: `device_links` table `id uuid PK, account_id uuid REFERENCES accounts(id), device_token_hash text, linked_at timestamptz NOT NULL DEFAULT now(), last_seen timestamptz` +- This task requires a real local Postgres reachable at `DATABASE_URL` — later tasks' tests assume the same. Document the prerequisite once here. + +- [ ] **Step 1: Add the `migrate` feature to sqlx** + +Edit `backend/accounts-service/Cargo.toml`, change the sqlx line to: + +```toml +sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres", "uuid", "chrono", "macros", "migrate"] } +``` + +- [ ] **Step 2: Write the migration** + +```sql +-- backend/accounts-service/migrations/0001_init.sql +CREATE TABLE accounts ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + email TEXT NOT NULL UNIQUE, + password_hash TEXT NOT NULL, + display_nick TEXT NOT NULL, + role TEXT NOT NULL DEFAULT 'user', + can_publish_addons BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE avatars ( + account_id UUID PRIMARY KEY REFERENCES accounts(id) ON DELETE CASCADE, + s3_key TEXT NOT NULL, + uploaded_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE device_links ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + account_id UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, + device_token_hash TEXT NOT NULL, + linked_at TIMESTAMPTZ NOT NULL DEFAULT now(), + last_seen TIMESTAMPTZ +); + +CREATE INDEX idx_device_links_account_id ON device_links(account_id); +``` + +`gen_random_uuid()` needs the `pgcrypto` extension. Add it at the top of the same file: + +```sql +CREATE EXTENSION IF NOT EXISTS pgcrypto; +``` + +(Prepend this line to `0001_init.sql`, before `CREATE TABLE accounts`.) + +- [ ] **Step 3: Ensure a local Postgres exists for tests** + +Document the one-time local setup (not a plan step to automate — infra prerequisite): + +```bash +# One-time, run manually before continuing: +docker run -d --name lovisual-pg -e POSTGRES_USER=lovisual -e POSTGRES_PASSWORD=lovisual \ + -e POSTGRES_DB=accounts_db -p 5432:5432 postgres:16 +``` + +- [ ] **Step 4: Write the failing test** + +```rust +// backend/accounts-service/tests/db_migration.rs +#[tokio::test] +async fn migrations_create_accounts_table() { + let database_url = std::env::var("DATABASE_URL") + .unwrap_or_else(|_| "postgres://lovisual:lovisual@localhost:5432/accounts_db".into()); + let pool = sqlx::PgPool::connect(&database_url).await.expect("connect"); + + sqlx::migrate!("./migrations").run(&pool).await.expect("migrate"); + + let row: (i64,) = sqlx::query_as("SELECT count(*) FROM accounts") + .fetch_one(&pool) + .await + .expect("accounts table must exist"); + assert_eq!(row.0, 0); +} +``` + +- [ ] **Step 5: Run test to verify it fails (before migrations exist it errors; run once the SQL file above is in place — this step confirms the table truly gets created, not that it's absent)** + +Run: `cd /storage/project/jvm/LoVisual/backend/accounts-service && DATABASE_URL=postgres://lovisual:lovisual@localhost:5432/accounts_db cargo test --test db_migration` +Expected: PASS (this task's "test" is really a smoke test that migrations apply cleanly — there's no meaningful pre-migration state to assert against, so we go straight to green and treat this as an integration checkpoint, not strict TDD red/green) + +- [ ] **Step 6: Commit** + +```bash +cd /storage/project/jvm/LoVisual +git add backend/accounts-service +git commit -m "feat(backend): accounts/avatars/device_links schema + migration" +``` + +--- + +### Task 3: Password hashing module + +**Files:** +- Create: `backend/accounts-service/src/auth/mod.rs` +- Create: `backend/accounts-service/src/auth/password.rs` +- Modify: `backend/accounts-service/src/lib.rs` (add `pub mod auth;`) + +**Interfaces:** +- Produces: `pub fn hash_password(plain: &str) -> Result` +- Produces: `pub fn verify_password(plain: &str, hash: &str) -> bool` +- Consumed by: Task 5 (register/login handlers) + +- [ ] **Step 1: Write the failing test** + +```rust +// backend/accounts-service/src/auth/password.rs (tests at the bottom of the same file) +use argon2::{Argon2, PasswordHasher, PasswordVerifier}; +use argon2::password_hash::phc::PasswordHash; + +pub fn hash_password(plain: &str) -> Result { + let argon2 = Argon2::default(); + Ok(argon2.hash_password(plain.as_bytes())?.to_string()) +} + +pub fn verify_password(plain: &str, hash: &str) -> bool { + let Ok(parsed) = PasswordHash::new(hash) else { return false }; + Argon2::default() + .verify_password(plain.as_bytes(), &parsed) + .is_ok() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn hash_then_verify_succeeds() { + let hash = hash_password("correct-horse-battery-staple").unwrap(); + assert!(verify_password("correct-horse-battery-staple", &hash)); + } + + #[test] + fn wrong_password_fails_verify() { + let hash = hash_password("correct-horse-battery-staple").unwrap(); + assert!(!verify_password("wrong-password", &hash)); + } + + #[test] + fn hash_is_not_the_plain_password() { + let hash = hash_password("secret123").unwrap(); + assert_ne!(hash, "secret123"); + } +} +``` + +- [ ] **Step 2: Wire the module** + +```rust +// backend/accounts-service/src/auth/mod.rs +pub mod password; +``` + +Add `pub mod auth;` to `backend/accounts-service/src/lib.rs` (near `pub mod config;`). + +- [ ] **Step 3: Run tests** + +Run: `cd /storage/project/jvm/LoVisual/backend/accounts-service && cargo test password::` +Expected: 3 tests pass + +- [ ] **Step 4: Commit** + +```bash +cd /storage/project/jvm/LoVisual +git add backend/accounts-service/src +git commit -m "feat(backend): argon2id password hashing" +``` + +--- + +### Task 4: JWT issue/verify + +**Files:** +- Create: `backend/accounts-service/src/auth/jwt.rs` +- Modify: `backend/accounts-service/src/auth/mod.rs` (add `pub mod jwt;`) + +**Interfaces:** +- Consumes: nothing new (takes `account_id: uuid::Uuid`, `secret: &str` as plain params — no dependency on DB types) +- Produces: `pub struct Claims { pub sub: String, pub exp: usize, pub token_type: String }` +- Produces: `pub fn issue_access_token(account_id: Uuid, secret: &str) -> String` (15 min expiry, `token_type: "access"`) +- Produces: `pub fn issue_refresh_token(account_id: Uuid, secret: &str) -> String` (30 day expiry, `token_type: "refresh"`) +- Produces: `pub fn verify_token(token: &str, secret: &str) -> Option` — returns `None` on any invalid/expired/malformed token, never panics + +- [ ] **Step 1: Write the failing test** + +```rust +// backend/accounts-service/src/auth/jwt.rs +use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Claims { + pub sub: String, + pub exp: usize, + pub token_type: String, +} + +fn issue(account_id: Uuid, secret: &str, token_type: &str, ttl_seconds: i64) -> String { + let exp = (chrono::Utc::now() + chrono::Duration::seconds(ttl_seconds)).timestamp() as usize; + let claims = Claims { sub: account_id.to_string(), exp, token_type: token_type.into() }; + encode(&Header::default(), &claims, &EncodingKey::from_secret(secret.as_bytes())) + .expect("encoding a well-formed Claims struct cannot fail") +} + +pub fn issue_access_token(account_id: Uuid, secret: &str) -> String { + issue(account_id, secret, "access", 15 * 60) +} + +pub fn issue_refresh_token(account_id: Uuid, secret: &str) -> String { + issue(account_id, secret, "refresh", 30 * 24 * 60 * 60) +} + +pub fn verify_token(token: &str, secret: &str) -> Option { + decode::( + token, + &DecodingKey::from_secret(secret.as_bytes()), + &Validation::default(), + ) + .ok() + .map(|data| data.claims) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn access_token_round_trips() { + let id = Uuid::new_v4(); + let token = issue_access_token(id, "test-secret"); + let claims = verify_token(&token, "test-secret").expect("should decode"); + assert_eq!(claims.sub, id.to_string()); + assert_eq!(claims.token_type, "access"); + } + + #[test] + fn wrong_secret_fails_verify() { + let token = issue_access_token(Uuid::new_v4(), "test-secret"); + assert!(verify_token(&token, "other-secret").is_none()); + } + + #[test] + fn garbage_token_fails_verify() { + assert!(verify_token("not.a.jwt", "test-secret").is_none()); + } +} +``` + +- [ ] **Step 2: Wire the module** + +Add `pub mod jwt;` to `backend/accounts-service/src/auth/mod.rs`. + +- [ ] **Step 3: Run tests** + +Run: `cd /storage/project/jvm/LoVisual/backend/accounts-service && cargo test jwt::` +Expected: 3 tests pass + +- [ ] **Step 4: Commit** + +```bash +cd /storage/project/jvm/LoVisual +git add backend/accounts-service/src +git commit -m "feat(backend): JWT access/refresh token issue and verify" +``` + +--- + +### Task 5: Accounts repository + +**Files:** +- Create: `backend/accounts-service/src/accounts/mod.rs` +- Create: `backend/accounts-service/src/accounts/model.rs` +- Create: `backend/accounts-service/src/accounts/repo.rs` +- Modify: `backend/accounts-service/src/lib.rs` (add `pub mod accounts;`) + +**Interfaces:** +- Consumes: `sqlx::PgPool` (from Task 1/2) +- Produces: `pub struct Account { pub id: Uuid, pub email: String, pub password_hash: String, pub display_nick: String, pub role: String, pub can_publish_addons: bool, pub created_at: chrono::DateTime }` +- Produces: `pub async fn create(pool: &PgPool, email: &str, password_hash: &str, nick: &str) -> Result` +- Produces: `pub async fn find_by_email(pool: &PgPool, email: &str) -> Result, sqlx::Error>` +- Produces: unique-violation on duplicate email surfaces as `sqlx::Error::Database` — callers (Task 6) match on `.constraint() == Some("accounts_email_key")` + +- [ ] **Step 1: Write the model** + +```rust +// backend/accounts-service/src/accounts/model.rs +use chrono::{DateTime, Utc}; +use serde::Serialize; +use uuid::Uuid; + +#[derive(Debug, Clone, sqlx::FromRow, Serialize)] +pub struct Account { + pub id: Uuid, + pub email: String, + #[serde(skip_serializing)] + pub password_hash: String, + pub display_nick: String, + pub role: String, + pub can_publish_addons: bool, + pub created_at: DateTime, +} +``` + +- [ ] **Step 2: Write the failing test (integration, needs the live Postgres from Task 2)** + +```rust +// backend/accounts-service/src/accounts/repo.rs +use super::model::Account; +use sqlx::PgPool; +use uuid::Uuid; + +pub async fn create( + pool: &PgPool, + email: &str, + password_hash: &str, + nick: &str, +) -> Result { + sqlx::query_as::<_, Account>( + "INSERT INTO accounts (email, password_hash, display_nick) + VALUES ($1, $2, $3) + RETURNING id, email, password_hash, display_nick, role, can_publish_addons, created_at", + ) + .bind(email) + .bind(password_hash) + .bind(nick) + .fetch_one(pool) + .await +} + +pub async fn find_by_email(pool: &PgPool, email: &str) -> Result, sqlx::Error> { + sqlx::query_as::<_, Account>( + "SELECT id, email, password_hash, display_nick, role, can_publish_addons, created_at + FROM accounts WHERE email = $1", + ) + .bind(email) + .fetch_optional(pool) + .await +} + +pub async fn find_by_id(pool: &PgPool, id: Uuid) -> Result, sqlx::Error> { + sqlx::query_as::<_, Account>( + "SELECT id, email, password_hash, display_nick, role, can_publish_addons, created_at + FROM accounts WHERE id = $1", + ) + .bind(id) + .fetch_optional(pool) + .await +} + +#[cfg(test)] +mod tests { + use super::*; + + async fn test_pool() -> PgPool { + let url = std::env::var("DATABASE_URL") + .unwrap_or_else(|_| "postgres://lovisual:lovisual@localhost:5432/accounts_db".into()); + let pool = PgPool::connect(&url).await.expect("connect"); + sqlx::migrate!("./migrations").run(&pool).await.expect("migrate"); + pool + } + + #[tokio::test] + async fn create_then_find_by_email_round_trips() { + let pool = test_pool().await; + let email = format!("test-{}@example.com", Uuid::new_v4()); + + let created = create(&pool, &email, "hash123", "TestNick").await.unwrap(); + assert_eq!(created.email, email); + assert_eq!(created.role, "user"); + assert!(created.can_publish_addons); + + let found = find_by_email(&pool, &email).await.unwrap().expect("must exist"); + assert_eq!(found.id, created.id); + + sqlx::query("DELETE FROM accounts WHERE id = $1") + .bind(created.id) + .execute(&pool) + .await + .unwrap(); + } + + #[tokio::test] + async fn find_by_email_returns_none_for_missing() { + let pool = test_pool().await; + let result = find_by_email(&pool, "does-not-exist@example.com").await.unwrap(); + assert!(result.is_none()); + } +} +``` + +- [ ] **Step 3: Wire the module** + +```rust +// backend/accounts-service/src/accounts/mod.rs +pub mod model; +pub mod repo; +``` + +Add `pub mod accounts;` to `backend/accounts-service/src/lib.rs`. + +- [ ] **Step 4: Run tests** + +Run: `cd /storage/project/jvm/LoVisual/backend/accounts-service && DATABASE_URL=postgres://lovisual:lovisual@localhost:5432/accounts_db cargo test accounts::repo::` +Expected: 2 tests pass + +- [ ] **Step 5: Commit** + +```bash +cd /storage/project/jvm/LoVisual +git add backend/accounts-service/src +git commit -m "feat(backend): accounts repository (create/find_by_email/find_by_id)" +``` + +--- + +### Task 6: `POST /auth/register` and `POST /auth/login` + +**Files:** +- Create: `backend/accounts-service/src/error.rs` +- Create: `backend/accounts-service/src/auth/handlers.rs` +- Modify: `backend/accounts-service/src/auth/mod.rs` (add `pub mod handlers;`) +- Modify: `backend/accounts-service/src/lib.rs` (`build_app` now takes routes + real pool; add `.route("/auth/register", post(...))` etc.) +- Test: `backend/accounts-service/tests/auth_flow.rs` + +**Interfaces:** +- Consumes: `auth::password::{hash_password, verify_password}` (Task 3), `auth::jwt::{issue_access_token, issue_refresh_token}` (Task 4), `accounts::repo::{create, find_by_email}` (Task 5) +- Produces: `AppError` enum implementing `axum::response::IntoResponse`, variants `Validation(String) -> 400`, `Conflict(String) -> 409`, `Unauthorized -> 401`, `Internal(anyhow::Error) -> 500` +- Produces: JSON contract — `POST /auth/register` body `{email, password, nick}` → `201 {id, email, display_nick}`; `POST /auth/login` body `{email, password}` → `200 {access_token, refresh_token}` + +- [ ] **Step 1: Write `error.rs`** + +```rust +// backend/accounts-service/src/error.rs +use axum::{http::StatusCode, response::{IntoResponse, Response}, Json}; +use serde_json::json; + +#[derive(Debug)] +pub enum AppError { + Validation(String), + Conflict(String), + Unauthorized, + Internal(anyhow::Error), +} + +impl IntoResponse for AppError { + fn into_response(self) -> Response { + let (status, message) = match self { + AppError::Validation(msg) => (StatusCode::BAD_REQUEST, msg), + AppError::Conflict(msg) => (StatusCode::CONFLICT, msg), + AppError::Unauthorized => (StatusCode::UNAUTHORIZED, "unauthorized".into()), + AppError::Internal(err) => { + tracing::error!("internal error: {err:?}"); + (StatusCode::INTERNAL_SERVER_ERROR, "internal error".into()) + } + }; + (status, Json(json!({ "error": message }))).into_response() + } +} + +impl From for AppError { + fn from(err: sqlx::Error) -> Self { + if let sqlx::Error::Database(ref db_err) = err { + if db_err.constraint() == Some("accounts_email_key") { + return AppError::Conflict("email already registered".into()); + } + } + AppError::Internal(err.into()) + } +} +``` + +- [ ] **Step 2: Write the failing test** + +```rust +// backend/accounts-service/tests/auth_flow.rs +use axum_test::TestServer; +use serde_json::json; +use uuid::Uuid; + +async fn test_pool() -> sqlx::PgPool { + let url = std::env::var("DATABASE_URL") + .unwrap_or_else(|_| "postgres://lovisual:lovisual@localhost:5432/accounts_db".into()); + let pool = sqlx::PgPool::connect(&url).await.expect("connect"); + sqlx::migrate!("../migrations").run(&pool).await.expect("migrate"); + pool +} + +#[tokio::test] +async fn register_then_login_succeeds() { + let pool = test_pool().await; + let app = accounts_service::build_app(pool.clone(), "test-secret".into()); + let server = TestServer::new(app); + let email = format!("flow-{}@example.com", Uuid::new_v4()); + + let register_response = server + .post("/auth/register") + .json(&json!({ "email": email, "password": "correct-horse-battery-staple", "nick": "Rider" })) + .await; + register_response.assert_status(axum::http::StatusCode::CREATED); + + let login_response = server + .post("/auth/login") + .json(&json!({ "email": email, "password": "correct-horse-battery-staple" })) + .await; + login_response.assert_status_ok(); + let body: serde_json::Value = login_response.json(); + assert!(body["access_token"].is_string()); + assert!(body["refresh_token"].is_string()); + + sqlx::query("DELETE FROM accounts WHERE email = $1") + .bind(&email) + .execute(&pool) + .await + .unwrap(); +} + +#[tokio::test] +async fn login_with_wrong_password_returns_401() { + let pool = test_pool().await; + let app = accounts_service::build_app(pool.clone(), "test-secret".into()); + let server = TestServer::new(app); + let email = format!("wrongpw-{}@example.com", Uuid::new_v4()); + + server + .post("/auth/register") + .json(&json!({ "email": email, "password": "right-password", "nick": "Rider" })) + .await + .assert_status(axum::http::StatusCode::CREATED); + + let login_response = server + .post("/auth/login") + .json(&json!({ "email": email, "password": "wrong-password" })) + .await; + login_response.assert_status(axum::http::StatusCode::UNAUTHORIZED); + + sqlx::query("DELETE FROM accounts WHERE email = $1") + .bind(&email) + .execute(&pool) + .await + .unwrap(); +} + +#[tokio::test] +async fn duplicate_email_registration_returns_409() { + let pool = test_pool().await; + let app = accounts_service::build_app(pool.clone(), "test-secret".into()); + let server = TestServer::new(app); + let email = format!("dup-{}@example.com", Uuid::new_v4()); + + server + .post("/auth/register") + .json(&json!({ "email": email, "password": "password123", "nick": "First" })) + .await + .assert_status(axum::http::StatusCode::CREATED); + + let second = server + .post("/auth/register") + .json(&json!({ "email": email, "password": "password456", "nick": "Second" })) + .await; + second.assert_status(axum::http::StatusCode::CONFLICT); + + sqlx::query("DELETE FROM accounts WHERE email = $1") + .bind(&email) + .execute(&pool) + .await + .unwrap(); +} +``` + +- [ ] **Step 3: Run the tests to see them fail to compile (build_app signature doesn't match yet)** + +Run: `cd /storage/project/jvm/LoVisual/backend/accounts-service && DATABASE_URL=postgres://lovisual:lovisual@localhost:5432/accounts_db cargo test --test auth_flow` +Expected: compile error, `build_app` takes 1 argument but 2 were supplied + +- [ ] **Step 4: Write the handlers** + +```rust +// backend/accounts-service/src/auth/handlers.rs +use crate::accounts::repo; +use crate::auth::{jwt, password}; +use crate::error::AppError; +use axum::{extract::State, http::StatusCode, Json}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone)] +pub struct AuthState { + pub pool: sqlx::PgPool, + pub jwt_secret: String, +} + +#[derive(Deserialize)] +pub struct RegisterRequest { + pub email: String, + pub password: String, + pub nick: String, +} + +#[derive(Serialize)] +pub struct RegisterResponse { + pub id: uuid::Uuid, + pub email: String, + pub display_nick: String, +} + +pub async fn register( + State(state): State, + Json(req): Json, +) -> Result<(StatusCode, Json), AppError> { + if req.password.len() < 8 { + return Err(AppError::Validation("password must be at least 8 characters".into())); + } + let hash = password::hash_password(&req.password) + .map_err(|e| AppError::Internal(anyhow::anyhow!(e)))?; + let account = repo::create(&state.pool, &req.email, &hash, &req.nick).await?; + Ok(( + StatusCode::CREATED, + Json(RegisterResponse { + id: account.id, + email: account.email, + display_nick: account.display_nick, + }), + )) +} + +#[derive(Deserialize)] +pub struct LoginRequest { + pub email: String, + pub password: String, +} + +#[derive(Serialize)] +pub struct LoginResponse { + pub access_token: String, + pub refresh_token: String, +} + +pub async fn login( + State(state): State, + Json(req): Json, +) -> Result, AppError> { + let account = repo::find_by_email(&state.pool, &req.email) + .await? + .ok_or(AppError::Unauthorized)?; + if !password::verify_password(&req.password, &account.password_hash) { + return Err(AppError::Unauthorized); + } + Ok(Json(LoginResponse { + access_token: jwt::issue_access_token(account.id, &state.jwt_secret), + refresh_token: jwt::issue_refresh_token(account.id, &state.jwt_secret), + })) +} +``` + +- [ ] **Step 5: Wire `build_app` in `lib.rs`** + +```rust +// backend/accounts-service/src/lib.rs +pub mod config; +pub mod error; +pub mod auth; +pub mod accounts; + +use auth::handlers::AuthState; +use axum::{routing::{get, post}, Router}; + +pub fn build_app(pool: sqlx::PgPool, jwt_secret: String) -> Router { + let state = AuthState { pool, jwt_secret }; + Router::new() + .route("/health", get(|| async { "ok" })) + .route("/auth/register", post(auth::handlers::register)) + .route("/auth/login", post(auth::handlers::login)) + .with_state(state) +} +``` + +Add `pub mod handlers;` to `backend/accounts-service/src/auth/mod.rs`. + +Update `backend/accounts-service/src/main.rs` call site: `build_app(pool, cfg.jwt_secret.clone())`. + +Update Task 1's `tests/health.rs` call site to match the new signature: `accounts_service::build_app(pool, "test-secret".into())`. + +- [ ] **Step 6: Run tests** + +Run: `cd /storage/project/jvm/LoVisual/backend/accounts-service && DATABASE_URL=postgres://lovisual:lovisual@localhost:5432/accounts_db cargo test` +Expected: all tests pass (health, jwt, password, accounts::repo, auth_flow) + +- [ ] **Step 7: Commit** + +```bash +cd /storage/project/jvm/LoVisual +git add backend/accounts-service +git commit -m "feat(backend): POST /auth/register and /auth/login" +``` + +--- + +### Task 7: Device Authorization Grant (`/device/code`, `/device/confirm`, `/device/token`) + +**Files:** +- Create: `backend/accounts-service/src/device/mod.rs` +- Create: `backend/accounts-service/src/device/store.rs` +- Create: `backend/accounts-service/src/device/handlers.rs` +- Modify: `backend/accounts-service/src/lib.rs` (mount routes, extend shared state) +- Test: `backend/accounts-service/tests/device_flow.rs` + +**Interfaces:** +- Consumes: `jwt::verify_token` (Task 4, to authenticate the `/device/confirm` caller — that endpoint requires a valid site access token in `Authorization: Bearer`) +- Produces: `DeviceStore` (in-memory `DashMap`, keyed by `device_code`), `pub fn new_pair() -> (String, String)` returning `(device_code, user_code)` where `user_code` is formatted `XXXX-XXXX` from `[A-Z0-9]` +- Produces: `POST /device/code` → `201 {device_code, user_code, expires_in: 600}` +- Produces: `POST /device/confirm` (auth required) body `{user_code}` → `200 {}` or `404` if code unknown/expired +- Produces: `POST /device/token` body `{device_code}` → `200 {device_token}` once confirmed, `202 {}` (still pending) before confirmation, `404` if unknown/expired + +- [ ] **Step 1: Write the device code store** + +```rust +// backend/accounts-service/src/device/store.rs +use dashmap::DashMap; +use rand::Rng; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use uuid::Uuid; + +#[derive(Clone)] +pub struct DeviceCodeEntry { + pub user_code: String, + pub confirmed_account_id: Option, + pub expires_at: Instant, +} + +#[derive(Clone, Default)] +pub struct DeviceStore { + by_device_code: Arc>, +} + +const TTL: Duration = Duration::from_secs(600); + +fn random_user_code() -> String { + use rand::RngExt; + const ALPHABET: &[u8] = b"ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // no O/0/I/1 confusion + let mut rng = rand::rng(); + let mut part = |n: usize| -> String { + (0..n).map(|_| ALPHABET[rng.random_range(0..ALPHABET.len())] as char).collect() + }; + format!("{}-{}", part(4), part(4)) +} + +impl DeviceStore { + pub fn create(&self) -> (String, String) { + let device_code = Uuid::new_v4().to_string(); + let user_code = random_user_code(); + self.by_device_code.insert( + device_code.clone(), + DeviceCodeEntry { + user_code: user_code.clone(), + confirmed_account_id: None, + expires_at: Instant::now() + TTL, + }, + ); + (device_code, user_code) + } + + /// Returns true if a matching, unexpired entry was found and confirmed. + pub fn confirm(&self, user_code: &str, account_id: Uuid) -> bool { + for mut entry in self.by_device_code.iter_mut() { + if entry.user_code == user_code && entry.expires_at > Instant::now() { + entry.confirmed_account_id = Some(account_id); + return true; + } + } + false + } + + /// None = unknown/expired. Some(None) = pending. Some(Some(id)) = confirmed. + pub fn poll(&self, device_code: &str) -> Option> { + let entry = self.by_device_code.get(device_code)?; + if entry.expires_at <= Instant::now() { + return None; + } + Some(entry.confirmed_account_id) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn create_returns_distinct_codes() { + let store = DeviceStore::default(); + let (dc1, uc1) = store.create(); + let (dc2, uc2) = store.create(); + assert_ne!(dc1, dc2); + assert_ne!(uc1, uc2); + } + + #[test] + fn confirm_then_poll_returns_account_id() { + let store = DeviceStore::default(); + let (device_code, user_code) = store.create(); + let account_id = Uuid::new_v4(); + + assert!(store.confirm(&user_code, account_id)); + assert_eq!(store.poll(&device_code), Some(Some(account_id))); + } + + #[test] + fn poll_before_confirm_is_pending() { + let store = DeviceStore::default(); + let (device_code, _user_code) = store.create(); + assert_eq!(store.poll(&device_code), Some(None)); + } + + #[test] + fn poll_unknown_code_returns_none() { + let store = DeviceStore::default(); + assert_eq!(store.poll("does-not-exist"), None); + } + + #[test] + fn confirm_unknown_user_code_returns_false() { + let store = DeviceStore::default(); + assert!(!store.confirm("ZZZZ-ZZZZ", Uuid::new_v4())); + } +} +``` + +- [ ] **Step 2: Run the store's unit tests** + +Run: `cd /storage/project/jvm/LoVisual/backend/accounts-service && cargo test device::store::` +Expected: 5 tests pass + +- [ ] **Step 3: Write the handlers** + +```rust +// backend/accounts-service/src/device/handlers.rs +use crate::device::store::DeviceStore; +use crate::error::AppError; +use axum::{extract::State, http::{HeaderMap, StatusCode}, Json}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone)] +pub struct DeviceState { + pub store: DeviceStore, + pub jwt_secret: String, +} + +#[derive(Serialize)] +pub struct DeviceCodeResponse { + pub device_code: String, + pub user_code: String, + pub expires_in: u64, +} + +pub async fn create_code( + State(state): State, +) -> (StatusCode, Json) { + let (device_code, user_code) = state.store.create(); + ( + StatusCode::CREATED, + Json(DeviceCodeResponse { device_code, user_code, expires_in: 600 }), + ) +} + +#[derive(Deserialize)] +pub struct ConfirmRequest { + pub user_code: String, +} + +fn account_id_from_auth_header(headers: &HeaderMap, secret: &str) -> Result { + let header = headers + .get(axum::http::header::AUTHORIZATION) + .and_then(|v| v.to_str().ok()) + .ok_or(AppError::Unauthorized)?; + let token = header.strip_prefix("Bearer ").ok_or(AppError::Unauthorized)?; + let claims = crate::auth::jwt::verify_token(token, secret).ok_or(AppError::Unauthorized)?; + claims.sub.parse().map_err(|_| AppError::Unauthorized) +} + +pub async fn confirm( + State(state): State, + headers: HeaderMap, + Json(req): Json, +) -> Result { + let account_id = account_id_from_auth_header(&headers, &state.jwt_secret)?; + if state.store.confirm(&req.user_code, account_id) { + Ok(StatusCode::OK) + } else { + Err(AppError::Validation("unknown or expired user_code".into())) + } +} + +#[derive(Deserialize)] +pub struct TokenRequest { + pub device_code: String, +} + +#[derive(Serialize)] +pub struct TokenResponse { + pub device_token: String, +} + +pub async fn token( + State(state): State, + Json(req): Json, +) -> Result<(StatusCode, Json>), AppError> { + match state.store.poll(&req.device_code) { + None => Err(AppError::Validation("unknown or expired device_code".into())), + Some(None) => Ok((StatusCode::ACCEPTED, Json(None))), + Some(Some(account_id)) => { + // The long-lived device_token is just a refresh-style JWT for now; + // Task 8+ (gateway plan) is where per-device revocation via + // device_links.device_token_hash gets enforced on every request. + let device_token = crate::auth::jwt::issue_refresh_token(account_id, &state.jwt_secret); + Ok((StatusCode::OK, Json(Some(TokenResponse { device_token })))) + } + } +} +``` + +- [ ] **Step 4: Write the module file** + +```rust +// backend/accounts-service/src/device/mod.rs +pub mod store; +pub mod handlers; +``` + +- [ ] **Step 5: Write the failing integration test** + +```rust +// backend/accounts-service/tests/device_flow.rs +use axum_test::TestServer; +use serde_json::json; +use uuid::Uuid; + +async fn test_pool() -> sqlx::PgPool { + let url = std::env::var("DATABASE_URL") + .unwrap_or_else(|_| "postgres://lovisual:lovisual@localhost:5432/accounts_db".into()); + let pool = sqlx::PgPool::connect(&url).await.expect("connect"); + sqlx::migrate!("../migrations").run(&pool).await.expect("migrate"); + pool +} + +#[tokio::test] +async fn full_device_link_flow() { + let pool = test_pool().await; + let app = accounts_service::build_app(pool.clone(), "test-secret".into()); + let server = TestServer::new(app); + let email = format!("device-{}@example.com", Uuid::new_v4()); + + server + .post("/auth/register") + .json(&json!({ "email": email, "password": "password123", "nick": "Rider" })) + .await + .assert_status(axum::http::StatusCode::CREATED); + let login: serde_json::Value = server + .post("/auth/login") + .json(&json!({ "email": email, "password": "password123" })) + .await + .json(); + let access_token = login["access_token"].as_str().unwrap(); + + let code_response: serde_json::Value = + server.post("/device/code").await.json(); + let device_code = code_response["device_code"].as_str().unwrap(); + let user_code = code_response["user_code"].as_str().unwrap(); + + let poll_before = server + .post("/device/token") + .json(&json!({ "device_code": device_code })) + .await; + poll_before.assert_status(axum::http::StatusCode::ACCEPTED); + + server + .post("/device/confirm") + .add_header( + axum::http::header::AUTHORIZATION, + format!("Bearer {access_token}"), + ) + .json(&json!({ "user_code": user_code })) + .await + .assert_status_ok(); + + let poll_after = server + .post("/device/token") + .json(&json!({ "device_code": device_code })) + .await; + poll_after.assert_status_ok(); + let token_body: serde_json::Value = poll_after.json(); + assert!(token_body["device_token"].is_string()); + + sqlx::query("DELETE FROM accounts WHERE email = $1") + .bind(&email) + .execute(&pool) + .await + .unwrap(); +} +``` + +- [ ] **Step 6: Wire routes in `lib.rs`** + +```rust +// backend/accounts-service/src/lib.rs +pub mod config; +pub mod error; +pub mod auth; +pub mod accounts; +pub mod device; + +use auth::handlers::AuthState; +use device::{handlers::DeviceState, store::DeviceStore}; +use axum::{routing::{get, post}, Router}; + +pub fn build_app(pool: sqlx::PgPool, jwt_secret: String) -> Router { + let auth_state = AuthState { pool: pool.clone(), jwt_secret: jwt_secret.clone() }; + let device_state = DeviceState { store: DeviceStore::default(), jwt_secret: jwt_secret.clone() }; + + let auth_routes = Router::new() + .route("/auth/register", post(auth::handlers::register)) + .route("/auth/login", post(auth::handlers::login)) + .with_state(auth_state); + + let device_routes = Router::new() + .route("/device/code", post(device::handlers::create_code)) + .route("/device/confirm", post(device::handlers::confirm)) + .route("/device/token", post(device::handlers::token)) + .with_state(device_state); + + Router::new() + .route("/health", get(|| async { "ok" })) + .merge(auth_routes) + .merge(device_routes) +} +``` + +- [ ] **Step 7: Run all tests** + +Run: `cd /storage/project/jvm/LoVisual/backend/accounts-service && DATABASE_URL=postgres://lovisual:lovisual@localhost:5432/accounts_db cargo test` +Expected: all pass, including `full_device_link_flow` + +- [ ] **Step 8: Commit** + +```bash +cd /storage/project/jvm/LoVisual +git add backend/accounts-service +git commit -m "feat(backend): OAuth device authorization grant for mod login" +``` + +--- + +### Task 8: Avatar upload (`POST /avatars`) + +**Files:** +- Create: `backend/accounts-service/src/avatars/mod.rs` +- Create: `backend/accounts-service/src/avatars/storage.rs` +- Create: `backend/accounts-service/src/avatars/handlers.rs` +- Modify: `backend/accounts-service/src/lib.rs` (mount route, extend `build_app` signature with S3 client) +- Test: `backend/accounts-service/tests/avatar_upload.rs` + +**Interfaces:** +- Consumes: `jwt::verify_token` (Task 4, auth on this endpoint), `accounts::repo` pattern extended with `update_avatar` (new function in this task) +- Produces: `pub struct S3Storage { client: aws_sdk_s3::Client, bucket: String }` with `pub async fn put(&self, key: &str, bytes: Vec, content_type: &str) -> anyhow::Result<()>` +- Produces: `POST /avatars` (multipart, auth required, field name `file`) → `200 {avatar_url}`; rejects >5MB with `413`, rejects non-image magic bytes with `400` +- This task requires a local MinIO for tests: `docker run -d --name lovisual-minio -p 9000:9000 -e MINIO_ROOT_USER=minioadmin -e MINIO_ROOT_PASSWORD=minioadmin minio/minio server /data` (one-time manual setup, same as Task 2's Postgres) + +- [ ] **Step 1: Write the S3 storage wrapper** + +```rust +// backend/accounts-service/src/avatars/storage.rs +use aws_sdk_s3::primitives::ByteStream; +use aws_sdk_s3::Client; + +#[derive(Clone)] +pub struct S3Storage { + pub client: Client, + pub bucket: String, +} + +impl S3Storage { + pub async fn from_config( + endpoint: &str, + access_key: &str, + secret_key: &str, + bucket: String, + ) -> Self { + let creds = aws_sdk_s3::config::Credentials::new(access_key, secret_key, None, None, "static"); + let config = aws_sdk_s3::config::Builder::new() + .endpoint_url(endpoint) + .credentials_provider(creds) + .region(aws_sdk_s3::config::Region::new("us-east-1")) + .force_path_style(true) + .behavior_version(aws_sdk_s3::config::BehaviorVersion::latest()) + .build(); + S3Storage { client: Client::from_conf(config), bucket } + } + + pub async fn put(&self, key: &str, bytes: Vec, content_type: &str) -> anyhow::Result<()> { + self.client + .put_object() + .bucket(&self.bucket) + .key(key) + .body(ByteStream::from(bytes)) + .content_type(content_type) + .send() + .await?; + Ok(()) + } +} +``` + +- [ ] **Step 2: Write the magic-byte content sniffer + handler** + +```rust +// backend/accounts-service/src/avatars/handlers.rs +use crate::avatars::storage::S3Storage; +use crate::error::AppError; +use axum::{extract::{Multipart, State}, http::HeaderMap, Json}; +use serde::Serialize; + +const MAX_AVATAR_BYTES: usize = 5 * 1024 * 1024; + +#[derive(Clone)] +pub struct AvatarState { + pub storage: S3Storage, + pub jwt_secret: String, + pub public_base_url: String, +} + +fn sniff_image_type(bytes: &[u8]) -> Option<&'static str> { + if bytes.starts_with(&[0x89, 0x50, 0x4E, 0x47]) { + Some("image/png") + } else if bytes.starts_with(&[0xFF, 0xD8, 0xFF]) { + Some("image/jpeg") + } else if bytes.len() > 12 && &bytes[8..12] == b"WEBP" { + Some("image/webp") + } else { + None + } +} + +fn account_id_from_auth_header(headers: &HeaderMap, secret: &str) -> Result { + let header = headers + .get(axum::http::header::AUTHORIZATION) + .and_then(|v| v.to_str().ok()) + .ok_or(AppError::Unauthorized)?; + let token = header.strip_prefix("Bearer ").ok_or(AppError::Unauthorized)?; + let claims = crate::auth::jwt::verify_token(token, secret).ok_or(AppError::Unauthorized)?; + claims.sub.parse().map_err(|_| AppError::Unauthorized) +} + +#[derive(Serialize)] +pub struct AvatarResponse { + pub avatar_url: String, +} + +pub async fn upload( + State(state): State, + headers: HeaderMap, + mut multipart: Multipart, +) -> Result, AppError> { + let account_id = account_id_from_auth_header(&headers, &state.jwt_secret)?; + + let field = multipart + .next_field() + .await + .map_err(|e| AppError::Validation(e.to_string()))? + .ok_or_else(|| AppError::Validation("missing file field".into()))?; + + let bytes = field + .bytes() + .await + .map_err(|e| AppError::Validation(e.to_string()))?; + + if bytes.len() > MAX_AVATAR_BYTES { + return Err(AppError::Validation("file too large (max 5MB)".into())); + } + + let content_type = sniff_image_type(&bytes) + .ok_or_else(|| AppError::Validation("unsupported image format".into()))?; + + let key = format!("avatars/{account_id}.bin"); + state + .storage + .put(&key, bytes.to_vec(), content_type) + .await + .map_err(AppError::Internal)?; + + Ok(Json(AvatarResponse { + avatar_url: format!("{}/{}", state.public_base_url, key), + })) +} +``` + +- [ ] **Step 3: Write the module file** + +```rust +// backend/accounts-service/src/avatars/mod.rs +pub mod storage; +pub mod handlers; +``` + +- [ ] **Step 4: Write the failing test** + +```rust +// backend/accounts-service/tests/avatar_upload.rs +use axum_test::TestServer; +use serde_json::json; +use uuid::Uuid; + +async fn test_pool() -> sqlx::PgPool { + let url = std::env::var("DATABASE_URL") + .unwrap_or_else(|_| "postgres://lovisual:lovisual@localhost:5432/accounts_db".into()); + let pool = sqlx::PgPool::connect(&url).await.expect("connect"); + sqlx::migrate!("../migrations").run(&pool).await.expect("migrate"); + pool +} + +// 1x1 red pixel PNG +const TINY_PNG: &[u8] = &[ + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, + 0xDE, 0x00, 0x00, 0x00, 0x0C, 0x49, 0x44, 0x41, 0x54, 0x08, 0xD7, 0x63, 0xF8, 0xCF, 0xC0, 0x00, + 0x00, 0x03, 0x01, 0x01, 0x00, 0x18, 0xDD, 0x8D, 0xB0, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, + 0x44, 0xAE, 0x42, 0x60, 0x82, +]; + +#[tokio::test] +async fn upload_valid_png_returns_avatar_url() { + let pool = test_pool().await; + let app = accounts_service::build_app(pool.clone(), "test-secret".into()).await; + let server = TestServer::new(app); + let email = format!("avatar-{}@example.com", Uuid::new_v4()); + + server + .post("/auth/register") + .json(&json!({ "email": email, "password": "password123", "nick": "Rider" })) + .await + .assert_status(axum::http::StatusCode::CREATED); + let login: serde_json::Value = server + .post("/auth/login") + .json(&json!({ "email": email, "password": "password123" })) + .await + .json(); + let access_token = login["access_token"].as_str().unwrap(); + + let response = server + .post("/avatars") + .add_header( + axum::http::header::AUTHORIZATION, + format!("Bearer {access_token}"), + ) + .multipart( + axum_test::multipart::MultipartForm::new().add_part( + "file", + axum_test::multipart::Part::bytes(TINY_PNG.to_vec()) + .file_name("avatar.png") + .mime_type("image/png"), + ), + ) + .await; + response.assert_status_ok(); + let body: serde_json::Value = response.json(); + assert!(body["avatar_url"].as_str().unwrap().contains("avatars/")); + + sqlx::query("DELETE FROM accounts WHERE email = $1") + .bind(&email) + .execute(&pool) + .await + .unwrap(); +} + +#[tokio::test] +async fn upload_non_image_bytes_returns_400() { + let pool = test_pool().await; + let app = accounts_service::build_app(pool.clone(), "test-secret".into()).await; + let server = TestServer::new(app); + let email = format!("badavatar-{}@example.com", Uuid::new_v4()); + + server + .post("/auth/register") + .json(&json!({ "email": email, "password": "password123", "nick": "Rider" })) + .await + .assert_status(axum::http::StatusCode::CREATED); + let login: serde_json::Value = server + .post("/auth/login") + .json(&json!({ "email": email, "password": "password123" })) + .await + .json(); + let access_token = login["access_token"].as_str().unwrap(); + + let response = server + .post("/avatars") + .add_header( + axum::http::header::AUTHORIZATION, + format!("Bearer {access_token}"), + ) + .multipart( + axum_test::multipart::MultipartForm::new().add_part( + "file", + axum_test::multipart::Part::bytes(b"not an image".to_vec()) + .file_name("fake.png") + .mime_type("image/png"), + ), + ) + .await; + response.assert_status(axum::http::StatusCode::BAD_REQUEST); + + sqlx::query("DELETE FROM accounts WHERE email = $1") + .bind(&email) + .execute(&pool) + .await + .unwrap(); +} +``` + +- [ ] **Step 5: Wire the route, make `build_app` async (S3 client setup is async) and thread config through** + +```rust +// backend/accounts-service/src/lib.rs +pub mod config; +pub mod error; +pub mod auth; +pub mod accounts; +pub mod device; +pub mod avatars; + +use auth::handlers::AuthState; +use avatars::{handlers::AvatarState, storage::S3Storage}; +use device::{handlers::DeviceState, store::DeviceStore}; +use axum::{extract::DefaultBodyLimit, routing::{get, post}, Router}; +use config::Config; + +pub async fn build_app(pool: sqlx::PgPool, cfg: Config) -> Router { + let auth_state = AuthState { pool: pool.clone(), jwt_secret: cfg.jwt_secret.clone() }; + let device_state = DeviceState { store: DeviceStore::default(), jwt_secret: cfg.jwt_secret.clone() }; + let storage = S3Storage::from_config( + &cfg.s3_endpoint, + &cfg.s3_access_key, + &cfg.s3_secret_key, + cfg.s3_bucket.clone(), + ) + .await; + let avatar_state = AvatarState { + storage, + jwt_secret: cfg.jwt_secret.clone(), + public_base_url: cfg.s3_endpoint.clone(), + }; + + let auth_routes = Router::new() + .route("/auth/register", post(auth::handlers::register)) + .route("/auth/login", post(auth::handlers::login)) + .with_state(auth_state); + + let device_routes = Router::new() + .route("/device/code", post(device::handlers::create_code)) + .route("/device/confirm", post(device::handlers::confirm)) + .route("/device/token", post(device::handlers::token)) + .with_state(device_state); + + let avatar_routes = Router::new() + .route("/avatars", post(avatars::handlers::upload)) + .layer(DefaultBodyLimit::max(6 * 1024 * 1024)) + .with_state(avatar_state); + + Router::new() + .route("/health", get(|| async { "ok" })) + .merge(auth_routes) + .merge(device_routes) + .merge(avatar_routes) +} +``` + +`build_app` is now `async` and takes `Config` instead of `(pool, jwt_secret)` — update every call site: +- `main.rs`: `let app = build_app(pool, cfg).await;` +- `tests/health.rs`, `tests/auth_flow.rs`, `tests/device_flow.rs`: build a `Config` with test values instead of passing `"test-secret".into()` directly, e.g.: + +```rust +fn test_config() -> accounts_service::config::Config { + accounts_service::config::Config { + database_url: String::new(), // unused, pool is passed separately + jwt_secret: "test-secret".into(), + port: 0, + s3_endpoint: "http://localhost:9000".into(), + s3_bucket: "lovisual-avatars-test".into(), + s3_access_key: "minioadmin".into(), + s3_secret_key: "minioadmin".into(), + } +} +``` + +and change every `accounts_service::build_app(pool.clone(), "test-secret".into())` to `accounts_service::build_app(pool.clone(), test_config()).await`. + +- [ ] **Step 6: Create the test bucket once, manually** + +```bash +docker exec lovisual-minio mkdir -p /data/lovisual-avatars-test +``` + +- [ ] **Step 7: Run all tests** + +Run: `cd /storage/project/jvm/LoVisual/backend/accounts-service && DATABASE_URL=postgres://lovisual:lovisual@localhost:5432/accounts_db S3_BUCKET=lovisual-avatars-test cargo test` +Expected: all pass + +- [ ] **Step 8: Commit** + +```bash +cd /storage/project/jvm/LoVisual +git add backend/accounts-service +git commit -m "feat(backend): avatar upload with magic-byte validation and S3 storage" +``` + +--- + +## What this plan does not cover (deliberately, next plans) + +- **API Gateway** (routing, single JWT check point, rate limiting per `TODO.md` Фаза 10 §6) — separate plan, depends on this one existing. +- **`configs-service`** (4 config slots, share codes, showcase) — separate plan, same pattern as this one. +- **React/Vite/Tailwind frontend** — separate plan, depends on the gateway plan for a stable API surface. +- **Refresh-token rotation/revocation and `device_links` persistence** (currently `/device/token` issues a JWT but never writes a row to the `device_links` table, so the "revoke a device" admin feature from `TODO.md` has nothing to revoke yet) — flagged here so it isn't forgotten; fold it into the gateway plan's auth-hardening pass, since revocation checks belong at the point every request is authenticated (the gateway), not duplicated in each service. diff --git a/backend/STRUCTURE.md b/backend/STRUCTURE.md new file mode 100644 index 0000000..7980bbe --- /dev/null +++ b/backend/STRUCTURE.md @@ -0,0 +1,44 @@ +# Backend Structure (продумано заранее, 2026-09-23) + +Rust workspace (`backend/Cargo.toml`), один сервис — один крейт. Планировка +на все три подсистемы платформы (см. `TODO.md`, Фаза 10) сразу, чтобы не +переизобретать layout при переходе от Подсистемы 1 к 2/3. + +``` +backend/ + Cargo.toml # workspace root — members растёт по мере реализации + STRUCTURE.md # этот файл + accounts-service/ # РЕАЛИЗУЕТСЯ — backend/PLAN.md (Подсистема 1, часть 1) + gateway/ # ПЛАН НЕ НАПИСАН — routing + единая точка JWT-проверки + # + рейт-лимиты (TODO.md §6). Начинается после + # accounts-service, т.к. фронтит уже готовый сервис. + configs-service/ # ПЛАН НЕ НАПИСАН — 4 слота конфигов, share-коды, + # витрина (Подсистема 1, часть 2) + chat-service/ # ПЛАН НЕ НАПИСАН — RPC-чат, друзья, presence, + # виджеты-телеметрия (Подсистема 2). Единственный + # сервис с WebSocket, а не только REST. + addons-registry/ # ПЛАН НЕ НАПИСАН — витрина аддонов, версии, + # публикация/модерация (Подсистема 3) +``` + +## Почему не добавлены в `[workspace] members` сразу +Пустой крейт без реализации — то же самое "без пустышек", что запрещено +правилами мода (см. `TODO.md`) — просто в Rust-обёртке: `cargo build +--workspace` не должен спотыкаться о нереализованные заглушки. Каждый +сервис входит в `members` в своём implementation-плане первым шагом (как +`accounts-service` в Task 1 `backend/PLAN.md`), не раньше. + +## Общий код между сервисами (`common`, пока не создан) +Кандидаты на вынос в `backend/common/` **когда появится второй реальный +потребитель** (не раньше — YAGNI): JWT `Claims`/`verify_token` (сейчас +только в `accounts-service`, но `gateway` тоже будет его проверять — при +старте `gateway` вынести в `common`), типовой `AppError`. До этого момента +дублирование двух сервисов — не проблема, преждевременная общая библиотека +между одним реальным потребителем — проблема. + +## Модульная структура внутри сервиса (эталон — `accounts-service`) +Домен, не технический слой: `auth/`, `accounts/`, `device/`, `avatars/` — +каждый со своими `mod.rs`/`handlers.rs`/`repo.rs` и т.п. (см. File Structure +в `backend/PLAN.md`). Тот же принцип — для `configs-service` (`slots/`, +`sharing/`, `showcase/`) и `chat-service` (`chat/`, `friends/`, +`telemetry/`) при их реализации. diff --git a/backend/accounts-service/.gitignore b/backend/accounts-service/.gitignore new file mode 100644 index 0000000..fedaa2b --- /dev/null +++ b/backend/accounts-service/.gitignore @@ -0,0 +1,2 @@ +/target +.env diff --git a/backend/accounts-service/Cargo.lock b/backend/accounts-service/Cargo.lock new file mode 100644 index 0000000..ff91ad7 --- /dev/null +++ b/backend/accounts-service/Cargo.lock @@ -0,0 +1,3931 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "accounts-service" +version = "0.1.0" +dependencies = [ + "anyhow", + "argon2", + "aws-config", + "aws-sdk-s3", + "axum", + "axum-test", + "chrono", + "dashmap", + "dotenvy", + "jsonwebtoken", + "rand 0.10.3", + "serde", + "serde_json", + "sqlx", + "tokio", + "tower-http", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arc-swap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "argon2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134c52ddac6d63c576bef8168db10c83c49c26444ecbc68060fef078925a901c" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures 0.3.1", + "password-hash", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "aws-config" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8d7b388a9fc3a6db15a5ec778c38b354eff1364882c94d08e0252f7a47dcaa4" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sdk-sso", + "aws-sdk-ssooidc", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "hex", + "http 1.5.0", + "sha1 0.10.7", + "time", + "tokio", + "tracing", + "url", + "zeroize", +] + +[[package]] +name = "aws-credential-types" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93964ffdaf57857f544be3666a5f57570bb699e934700f11b49708f61bb556e" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "zeroize", +] + +[[package]] +name = "aws-lc-rs" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "aws-runtime" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b8a9911551b4ea6ca13805ef52ed96f7d2bbb43cc3b4a14cb0776a71f33cfaa" +dependencies = [ + "aws-credential-types", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "bytes-utils", + "fastrand", + "http 1.5.0", + "http-body 1.1.0", + "percent-encoding", + "pin-project-lite", + "tracing", + "uuid", +] + +[[package]] +name = "aws-sdk-s3" +version = "1.149.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d48257fb0eb1907a02a965ff7d1b910770a6de2c59a2d03461678df989c5cdcd" +dependencies = [ + "arc-swap", + "aws-credential-types", + "aws-runtime", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-checksums", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "bytes", + "fastrand", + "hex", + "hmac 0.13.0", + "http 1.5.0", + "http-body 1.1.0", + "lru", + "percent-encoding", + "regex-lite", + "sha2 0.11.0", + "tracing", + "url", +] + +[[package]] +name = "aws-sdk-sso" +version = "1.112.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d59c47556b283bb2d20fc0b416df7d9d9a3d6f5fa0e77867167a3f2efc23b9" +dependencies = [ + "arc-swap", + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 1.5.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-ssooidc" +version = "1.114.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eceb9f31b46ba6da21f666e456395624824a2a086caed940b13d78832fa3ee02" +dependencies = [ + "arc-swap", + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 1.5.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sts" +version = "1.117.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9291df9ea767ab5e77d08a53f4f22daa917909a34ed7a22c1a4b82d193a423ad" +dependencies = [ + "arc-swap", + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "fastrand", + "http 1.5.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sigv4" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2312577f088c9fbf4206dfdb884cf1de9407b43e1a923cbed5237775116fc24b" +dependencies = [ + "aws-credential-types", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "crypto-bigint", + "form_urlencoded", + "hex", + "hmac 0.13.0", + "http 1.5.0", + "p256", + "percent-encoding", + "sha2 0.11.0", + "subtle", + "time", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-async" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02e407fb3b54891734224b9ffac8a71fdd35f542500fa1af95754a6b2beb316" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "aws-smithy-checksums" +version = "0.65.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67ecd999972b58e67cab052f5129906c08c25883bd0788ceefc55ef97d61307" +dependencies = [ + "aws-smithy-http", + "aws-smithy-types", + "bytes", + "crc-fast", + "hex", + "http 1.5.0", + "http-body 1.1.0", + "http-body-util", + "md-5", + "pin-project-lite", + "sha1 0.11.0", + "sha2 0.11.0", + "tracing", +] + +[[package]] +name = "aws-smithy-eventstream" +version = "0.61.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80c2051c2f1016fb8e6548dd07b8bc2ac9c3fe583721444b92f515e856d31609" +dependencies = [ + "aws-smithy-types", + "bytes", + "crc32fast", +] + +[[package]] +name = "aws-smithy-http" +version = "0.64.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37843d9add67c3aff5856f409c6dc315d3cdff60f9c0cb5b670dab1e9920306d" +dependencies = [ + "aws-smithy-eventstream", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "futures-util", + "http 1.5.0", + "http-body 1.1.0", + "http-body-util", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-http-client" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bd25384a4e437aa8d8f339afad4b69e786b936a7cb10db668a7aaf66717b1a8" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "h2 0.3.27", + "h2 0.4.19", + "http 0.2.12", + "http 1.5.0", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper 1.11.1", + "hyper-rustls 0.24.2", + "hyper-rustls 0.27.10", + "hyper-util", + "pin-project-lite", + "rustls 0.21.12", + "rustls 0.23.45", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.5", + "tower", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.63.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3385d469edbe8b60cc72002784652b5efca39178192aa9cc4b44c9875c6bdc18" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-observability" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e86338c869539a581bf161247762a6e87f92c5c075060057b5ed6d06632ed0c" +dependencies = [ + "aws-smithy-runtime-api", +] + +[[package]] +name = "aws-smithy-query" +version = "0.62.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1d1d71f6562be974caa85442ecd90194c40fdb5df045f182a6c2e872ce95056" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "aws-smithy-xml", + "urlencoding", +] + +[[package]] +name = "aws-smithy-runtime" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "591e0024cdf2a8de8711860f7e1d5cfbca26b800ce13793a9aa2f4f5918e6d95" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-http-client", + "aws-smithy-observability", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "bytes", + "fastrand", + "http 1.5.0", + "http-body 1.1.0", + "http-body-util", + "pin-project-lite", + "pin-utils", + "tokio", + "tracing", +] + +[[package]] +name = "aws-smithy-runtime-api" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf162725183ec0df77a9dc82ce22d9ba0b6ea7f376e8ec4f6b695727c70698" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api-macros", + "aws-smithy-types", + "bytes", + "http 0.2.12", + "http 1.5.0", + "pin-project-lite", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-runtime-api-macros" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "221eaa237ddf1ca79b60d1372aad77e47f9c0ea5b3ce5099da8c61d027dc77b3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "aws-smithy-schema" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8f395d93304280b64b7632fea798d177e74897fe7f063416ce627cd6fa24829" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "http 1.5.0", +] + +[[package]] +name = "aws-smithy-types" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16ecf7989e381d0cbb7ca06104140d940bf38d8eb9004bd3a12d23df03e3d363" +dependencies = [ + "base64-simd", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http 1.5.0", + "http-body 0.4.6", + "http-body 1.1.0", + "http-body-util", + "itoa", + "num-integer", + "pin-project-lite", + "pin-utils", + "ryu", + "serde", + "time", + "tokio", + "tokio-util", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.62.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b932c8d6dc127fc980eecd78f8694ae9b9551b69a93a7def2a199c1c0033daf" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "209f3a6d82a6e9e5f94abbed94c7a26e1c052341002bf57a5fb5481f625896fc" +dependencies = [ + "aws-credential-types", + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "rustc_version", + "tracing", +] + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http 1.5.0", + "http-body 1.1.0", + "http-body-util", + "hyper 1.11.1", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "multer", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http 1.5.0", + "http-body 1.1.0", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-test" +version = "21.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bf0893561659ea1365921a0aed8bbb620fae83dae00b53fe64d41fea4877640" +dependencies = [ + "anyhow", + "axum", + "bytes", + "bytesize", + "cookie", + "educe", + "expect-json", + "http 1.5.0", + "http-body-util", + "hyper 1.11.1", + "hyper-util", + "mime", + "pretty_assertions", + "reserve-port", + "rust-multipart-rfc7578_2", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tower", + "url", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" +dependencies = [ + "serde_core", +] + +[[package]] +name = "blake2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b5d4d889834ee8ecfc0f8426ad30faf7cdcb10f741a8e6d7224d95325479f6f" +dependencies = [ + "digest 0.11.3", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "bytes-utils" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" +dependencies = [ + "bytes", + "either", +] + +[[package]] +name = "bytesize" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7354288c522e7e980fafd2075d63d1285794c3a6a16cdd492f189ea406e5f18b" + +[[package]] +name = "cc" +version = "1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54413ede23c2daf518f35156dfde027feb2374004d63bd497f983c8db9c0e313" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "cookie" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + +[[package]] +name = "crc-fast" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e75b2483e97a5a7da73ac68a05b629f9c53cff58d8ed1c77866079e18b00dba5" +dependencies = [ + "digest 0.10.7", + "spin 0.10.1", +] + +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e8bd762f7479489c70ed6c768ddca99d7296857de437a68dcb2a94365b3fae" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dashmap" +version = "6.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid 0.9.6", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.6", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid 0.10.2", + "crypto-common 0.2.2", + "ctutils", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2 0.10.9", + "subtle", + "zeroize", +] + +[[package]] +name = "educe" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e451fac8dd8dece16234604bf1efce6e90fddd8ab6ad4d66eec0eca5160959dd" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" +dependencies = [ + "serde", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "hkdf 0.12.4", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +dependencies = [ + "serde", +] + +[[package]] +name = "encoding_rs" +version = "0.8.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5ef0006ac9ab233c38522f5ae99cae3625151de8f706cacee1cba4b8e2832a" +dependencies = [ + "cfg-if", + "core_detect", + "multiversion", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", +] + +[[package]] +name = "enum-ordinalize" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89dd01549b09589510cf0647475075d12071456586d70f5c75c98ae2a5537677" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65863d15a4ce2888bd2f0f543cc963d3879c3a022c8ee43f6141d479a3ac815" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etcetera" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" +dependencies = [ + "cfg-if", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "expect-json" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e80819dbfe83c8a651f5344b08910d0037dac72988aef27ee4e6bedd7ae2e33" +dependencies = [ + "chrono", + "email_address", + "expect-json-macros", + "num", + "regex", + "serde", + "serde_json", + "thiserror", + "typetag", + "uuid", +] + +[[package]] +name = "expect-json-macros" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0637949cd816934f3b7aab44ff98e7ec1fb903c379e07dcb9eac943ec33499e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b" + +[[package]] +name = "flume" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +dependencies = [ + "futures-core", + "futures-sink", + "spin 0.9.9", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "rand_core 0.10.1", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.5.0", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac 0.12.1", +] + +[[package]] +name = "hkdf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" +dependencies = [ + "hmac 0.13.0", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http 1.5.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http 1.5.0", + "http-body 1.1.0", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2 0.4.19", + "http 1.5.0", + "http-body 1.1.0", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.32", + "log", + "rustls 0.21.12", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53" +dependencies = [ + "http 1.5.0", + "hyper 1.11.1", + "hyper-util", + "rustls 0.23.45", + "rustls-native-certs", + "tokio", + "tokio-rustls 0.26.5", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http 1.5.0", + "http-body 1.1.0", + "hyper 1.11.1", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.5", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "inventory" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "11.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e75fe14a82d81e5f5af639997db37d8b96045938a7ac6ab18cdbe1c7467e05e1" +dependencies = [ + "base64", + "ed25519-dalek", + "getrandom 0.2.17", + "hmac 0.12.1", + "js-sys", + "p256", + "p384", + "rand 0.8.8", + "rsa", + "serde", + "serde_json", + "sha2 0.10.9", + "signature", + "zeroize", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin 0.9.9", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libsqlite3-sys" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "lru" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef9ac18847474e638e3702b76c65d4eb93428471a74778ef0f1be711717f89b5" +dependencies = [ + "hashbrown 0.17.1", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "md-5" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest 0.11.3", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http 1.5.0", + "httparse", + "memchr", + "mime", + "spin 0.9.9", + "version_check", +] + +[[package]] +name = "multiversion" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ca4bea16ffc3f443cf7d866912118196bfef4c6a1556ca00f9f9b00bb43f7c" +dependencies = [ + "multiversion-macros", +] + +[[package]] +name = "multiversion-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d416831a7317ef4b08bee00b69cbbb9c8763da7959a7026244d6266869f9c83" +dependencies = [ + "proc-macro2", + "quote", + "rustversion", + "syn 3.0.6", +] + +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.8", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "password-hash" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aab41826031698d6ffcd9cff78ef56ef998e39dc7e5067cdfebe373842d4723b" +dependencies = [ + "getrandom 0.4.3", + "phc", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phc" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44dc769b75f93afdddd8c7fa12d685292ddeff1e66f7f0f3a234cf1818afe892" +dependencies = [ + "base64ct", + "ctutils", + "getrandom 0.4.3", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reserve-port" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71ea98a177596a4579881992bd2bd4af27772fc95d0e5f5668a8f9535eca6380" +dependencies = [ + "thiserror", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac 0.12.1", + "subtle", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid 0.9.6", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rust-multipart-rfc7578_2" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00bdaa068902270ca7fa8619775e1838e23a63620abac0947ce0f715819b8cec" +dependencies = [ + "bytes", + "futures-core", + "futures-util", + "http 1.5.0", + "mime", + "rand 0.10.3", + "thiserror", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + +[[package]] +name = "rustls" +version = "0.23.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +dependencies = [ + "aws-lc-rs", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki 0.103.15", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "digest 0.11.3", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "digest 0.11.3", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spin" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "378620ccc25c62c89d8be1c819e76a88d59bdcc3304733330788948e619bfd71" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b44e85bf579a8eeb4ceaa77a3a523baf2bf0e9bac7e40f405d537b5d2d5ccb" +dependencies = [ + "base64", + "bytes", + "cfg-if", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.16.1", + "hashlink", + "indexmap", + "log", + "memchr", + "percent-encoding", + "rustls 0.23.45", + "serde", + "serde_json", + "sha2 0.10.9", + "smallvec", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", + "webpki-roots", +] + +[[package]] +name = "sqlx-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd2b84f2bc39a5705ef27ec785a11c934a41bbd4a24941e257927cddc26b60bf" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.119", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb8d96de5fdc85a5c4ec813432b523ec637e80ba98f046555f75f7908ddac7c3" +dependencies = [ + "cfg-if", + "dotenvy", + "either", + "heck", + "hex", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.9", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.119", + "thiserror", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90b8020fe17c5f2c245bfa2505d7ef59c5604839527c740266ad2214acebea27" +dependencies = [ + "bitflags", + "byteorder", + "bytes", + "chrono", + "crc", + "digest 0.11.3", + "dotenvy", + "either", + "futures-core", + "futures-util", + "generic-array", + "log", + "percent-encoding", + "serde", + "sha1 0.11.0", + "sha2 0.11.0", + "sqlx-core", + "thiserror", + "tracing", + "uuid", +] + +[[package]] +name = "sqlx-postgres" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87a2bdd6e83f6b3ea525ca9fee568030508b58355a43d0b2c1674d5f79dcd65e" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf 0.13.0", + "hmac 0.13.0", + "itoa", + "log", + "md-5", + "memchr", + "rand 0.10.3", + "serde", + "serde_json", + "sha2 0.11.0", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488e99c397a62007e4229aec669a179816339afc6d2620ca6fa420dbee2e982c" +dependencies = [ + "atoi", + "chrono", + "flume", + "form_urlencoded", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "thiserror", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.5", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" +dependencies = [ + "rustls 0.23.45", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08a05a66a4fdd61cbbe0a1d755ffe0ca6aba159dd4820936a0ff8a8278245b9c" +dependencies = [ + "bitflags", + "bytes", + "http 1.5.0", + "http-body 1.1.0", + "percent-encoding", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "typetag" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c90e86058a30d42a1a928dfb4b49bb33c98c3a2b4909492e6b0881cd94798ec2" +dependencies = [ + "erased-serde", + "inventory", + "once_cell", + "serde", + "typetag-impl", +] + +[[package]] +name = "typetag-impl" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f153acc4e99a5f2a5aefa09fb078be54e26271b2813f6041200b224c098d8328" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.6", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626c4bac6755d76ffc12cb01b2eac751db1996b9e0041de9aa02c8c211ddc82c" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d35102a9f36d089ccae9e4c6802bc118be4487b80aaffc0ab4e0cf5ce92d2873" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c01f5ab44258da43cf276c74a2763db2ff3969c9c652c3f2de07041d0b2bc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/backend/accounts-service/Cargo.toml b/backend/accounts-service/Cargo.toml new file mode 100644 index 0000000..13e9522 --- /dev/null +++ b/backend/accounts-service/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "accounts-service" +version = "0.1.0" +edition = "2024" + +[lib] +name = "accounts_service" +path = "src/lib.rs" + +[dependencies] +axum = { version = "0.8", features = ["multipart"] } +tokio = { version = "1", features = ["rt-multi-thread", "macros"] } +tower-http = { version = "0.7", features = ["trace", "cors"] } +tracing = "0.1" +tracing-subscriber = "0.3" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +sqlx = { version = "0.9", default-features = false, features = ["runtime-tokio", "tls-rustls", "postgres", "uuid", "chrono", "macros", "migrate"] } +uuid = { version = "1", features = ["v4", "serde"] } +chrono = { version = "0.4", features = ["serde"] } +argon2 = "0.6" +jsonwebtoken = { version = "11", default-features = false, features = ["rust_crypto"] } +rand = "0.10" +dashmap = "6" +aws-sdk-s3 = "1" +aws-config = "1" +anyhow = "1" +dotenvy = "0.15" + +[dev-dependencies] +axum-test = "21" diff --git a/backend/accounts-service/migrations/0001_init.sql b/backend/accounts-service/migrations/0001_init.sql new file mode 100644 index 0000000..56810f3 --- /dev/null +++ b/backend/accounts-service/migrations/0001_init.sql @@ -0,0 +1,27 @@ +CREATE EXTENSION IF NOT EXISTS pgcrypto; + +CREATE TABLE accounts ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + email TEXT NOT NULL UNIQUE, + password_hash TEXT NOT NULL, + display_nick TEXT NOT NULL, + role TEXT NOT NULL DEFAULT 'user', + can_publish_addons BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE avatars ( + account_id UUID PRIMARY KEY REFERENCES accounts(id) ON DELETE CASCADE, + s3_key TEXT NOT NULL, + uploaded_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE device_links ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + account_id UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, + device_token_hash TEXT NOT NULL, + linked_at TIMESTAMPTZ NOT NULL DEFAULT now(), + last_seen TIMESTAMPTZ +); + +CREATE INDEX idx_device_links_account_id ON device_links(account_id); diff --git a/backend/accounts-service/src/auth/mod.rs b/backend/accounts-service/src/auth/mod.rs new file mode 100644 index 0000000..c72e4b9 --- /dev/null +++ b/backend/accounts-service/src/auth/mod.rs @@ -0,0 +1 @@ +pub mod password; diff --git a/backend/accounts-service/src/auth/password.rs b/backend/accounts-service/src/auth/password.rs new file mode 100644 index 0000000..fc74dbd --- /dev/null +++ b/backend/accounts-service/src/auth/password.rs @@ -0,0 +1,37 @@ +use argon2::{Argon2, PasswordHasher, PasswordVerifier}; +use argon2::password_hash::phc::PasswordHash; + +pub fn hash_password(plain: &str) -> Result { + let argon2 = Argon2::default(); + Ok(argon2.hash_password(plain.as_bytes())?.to_string()) +} + +pub fn verify_password(plain: &str, hash: &str) -> bool { + let Ok(parsed) = PasswordHash::new(hash) else { return false }; + Argon2::default() + .verify_password(plain.as_bytes(), &parsed) + .is_ok() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn hash_then_verify_succeeds() { + let hash = hash_password("correct-horse-battery-staple").unwrap(); + assert!(verify_password("correct-horse-battery-staple", &hash)); + } + + #[test] + fn wrong_password_fails_verify() { + let hash = hash_password("correct-horse-battery-staple").unwrap(); + assert!(!verify_password("wrong-password", &hash)); + } + + #[test] + fn hash_is_not_the_plain_password() { + let hash = hash_password("secret123").unwrap(); + assert_ne!(hash, "secret123"); + } +} diff --git a/backend/accounts-service/src/config.rs b/backend/accounts-service/src/config.rs new file mode 100644 index 0000000..2171a1b --- /dev/null +++ b/backend/accounts-service/src/config.rs @@ -0,0 +1,35 @@ +use anyhow::{Context, Result}; + +#[derive(Clone)] +pub struct Config { + pub database_url: String, + pub jwt_secret: String, + pub port: u16, + pub s3_endpoint: String, + pub s3_bucket: String, + pub s3_access_key: String, + pub s3_secret_key: String, +} + +impl Config { + pub fn from_env() -> Result { + Ok(Config { + database_url: std::env::var("DATABASE_URL") + .context("DATABASE_URL not set")?, + jwt_secret: std::env::var("JWT_SECRET") + .context("JWT_SECRET not set")?, + port: std::env::var("PORT") + .unwrap_or_else(|_| "8081".into()) + .parse() + .context("PORT must be a number")?, + s3_endpoint: std::env::var("S3_ENDPOINT") + .context("S3_ENDPOINT not set")?, + s3_bucket: std::env::var("S3_BUCKET") + .context("S3_BUCKET not set")?, + s3_access_key: std::env::var("S3_ACCESS_KEY") + .context("S3_ACCESS_KEY not set")?, + s3_secret_key: std::env::var("S3_SECRET_KEY") + .context("S3_SECRET_KEY not set")?, + }) + } +} diff --git a/backend/accounts-service/src/lib.rs b/backend/accounts-service/src/lib.rs new file mode 100644 index 0000000..4cd5db8 --- /dev/null +++ b/backend/accounts-service/src/lib.rs @@ -0,0 +1,8 @@ +pub mod auth; +pub mod config; + +use axum::{routing::get, Router}; + +pub fn build_app(_pool: sqlx::PgPool) -> Router { + Router::new().route("/health", get(|| async { "ok" })) +} diff --git a/backend/accounts-service/src/main.rs b/backend/accounts-service/src/main.rs new file mode 100644 index 0000000..356aa0e --- /dev/null +++ b/backend/accounts-service/src/main.rs @@ -0,0 +1,19 @@ +use accounts_service::{build_app, config::Config}; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt::init(); + dotenvy::dotenv().ok(); + let cfg = Config::from_env()?; + + let pool = sqlx::postgres::PgPoolOptions::new() + .max_connections(10) + .connect(&cfg.database_url) + .await?; + + let app = build_app(pool); + let listener = tokio::net::TcpListener::bind(("0.0.0.0", cfg.port)).await?; + tracing::info!("accounts-service listening on {}", cfg.port); + axum::serve(listener, app).await?; + Ok(()) +} diff --git a/backend/accounts-service/tests/db_migration.rs b/backend/accounts-service/tests/db_migration.rs new file mode 100644 index 0000000..2e35183 --- /dev/null +++ b/backend/accounts-service/tests/db_migration.rs @@ -0,0 +1,14 @@ +#[tokio::test] +async fn migrations_create_accounts_table() { + let database_url = std::env::var("DATABASE_URL") + .unwrap_or_else(|_| "postgres://lovisual:lovisual@localhost:5432/accounts_db".into()); + let pool = sqlx::PgPool::connect(&database_url).await.expect("connect"); + + sqlx::migrate!("./migrations").run(&pool).await.expect("migrate"); + + let row: (i64,) = sqlx::query_as("SELECT count(*) FROM accounts") + .fetch_one(&pool) + .await + .expect("accounts table must exist"); + assert_eq!(row.0, 0); +} diff --git a/backend/accounts-service/tests/health.rs b/backend/accounts-service/tests/health.rs new file mode 100644 index 0000000..6f4ecfb --- /dev/null +++ b/backend/accounts-service/tests/health.rs @@ -0,0 +1,16 @@ +use axum_test::TestServer; + +#[tokio::test] +async fn health_returns_ok() { + // NOTE: this test does not touch the DB — pass a pool that is never + // queried. sqlx::PgPool::connect_lazy never opens a connection until + // a query runs, so this is safe without a running Postgres. + let pool = sqlx::PgPool::connect_lazy("postgres://user:pass@localhost/db") + .expect("lazy pool"); + let app = accounts_service::build_app(pool); + let server = TestServer::new(app); + + let response = server.get("/health").await; + response.assert_status_ok(); + response.assert_text("ok"); +} diff --git a/backend/addons-registry/README.md b/backend/addons-registry/README.md new file mode 100644 index 0000000..06ccc16 --- /dev/null +++ b/backend/addons-registry/README.md @@ -0,0 +1,5 @@ +# addons-registry (planned) + +Addon marketplace backend: versions, publishing, moderation (Подсистема 3). +See `TODO.md` (Фаза 10, "Подсистема 3") and `backend/STRUCTURE.md`. No +implementation plan yet. diff --git a/backend/chat-service/README.md b/backend/chat-service/README.md new file mode 100644 index 0000000..ef36036 --- /dev/null +++ b/backend/chat-service/README.md @@ -0,0 +1,5 @@ +# chat-service (planned) + +RPC chat, friends, presence, telemetry widgets (Подсистема 2). WebSocket, not +just REST — see `TODO.md` (Фаза 10, "Подсистема 2") and +`backend/STRUCTURE.md`. No implementation plan yet. diff --git a/backend/configs-service/README.md b/backend/configs-service/README.md new file mode 100644 index 0000000..c4c3c03 --- /dev/null +++ b/backend/configs-service/README.md @@ -0,0 +1,5 @@ +# configs-service (planned) + +4 cloud config slots per account, share codes, public showcase (Подсистема 1, +часть 2). See `TODO.md` (Фаза 10) and `backend/STRUCTURE.md`. No +implementation plan yet. diff --git a/backend/gateway/README.md b/backend/gateway/README.md new file mode 100644 index 0000000..650d52f --- /dev/null +++ b/backend/gateway/README.md @@ -0,0 +1,6 @@ +# gateway (planned) + +Routing + single JWT check point + rate limits in front of `accounts-service`, +`configs-service`, `chat-service`. See `TODO.md` (Фаза 10) and +`backend/STRUCTURE.md`. No implementation plan yet — starts after +`accounts-service` (`backend/PLAN.md`) is done. diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/ModulesMenuScreen.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/ModulesMenuScreen.java deleted file mode 100644 index 9dd4e09..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/ModulesMenuScreen.java +++ /dev/null @@ -1,966 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.features.gui.clickgui.layout.screen.modules; - -import dev.loki.lovisual.config.i18n.MainConfig; -import dev.loki.lovisual.render.engine.renderer.warp.RenderWarpStack; -import net.minecraft.client.Minecraft; -import org.lwjgl.glfw.GLFW; -import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; -import dev.loki.lovisual.features.gui.clickgui.tabs.ClickGuiSearch; -import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.module.ModuleComponent; -import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.LayoutRender2D; -import dev.loki.lovisual.features.gui.clickgui.settings.core.Setting; -import dev.loki.lovisual.features.gui.clickgui.settings.render.SettingRenderContext; -import dev.loki.lovisual.features.gui.clickgui.settings.render.SettingRenderSurface; -import dev.loki.lovisual.features.gui.clickgui.util.tooltip.ClickGuiHintOverlay; -import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiI18n; -import dev.loki.lovisual.render.engine.animation.AnimationUtility; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.svg.model.SvgRenderOptions; -import dev.loki.lovisual.render.engine.text.font.info.FontInfo; -import dev.loki.lovisual.render.engine.text.font.Fonts; -import dev.loki.lovisual.render.engine.text.render.TextRenderer; -import dev.loki.lovisual.render.helpers.clip.ClipFunction; -import dev.loki.lovisual.render.helpers.clip.ScissorFunction; -import dev.loki.lovisual.render.helpers.util.SystemCursor; -import dev.loki.lovisual.util.logging.DebugLog; - -import java.util.ArrayList; -import java.util.List; -import dev.loki.lovisual.features.gui.hud.draggable.impl.panels.ModuleList; -import dev.loki.lovisual.features.gui.clickgui.layout.screen.modules.model.ModulesMenuCategory; -import dev.loki.lovisual.features.gui.clickgui.layout.screen.modules.ModulesMenuPanel; -import dev.loki.lovisual.features.gui.clickgui.layout.screen.modules.ModulesMenuResolver; -import dev.loki.lovisual.features.gui.clickgui.layout.screen.modules.model.ModulesMenuStyle; -import dev.loki.lovisual.features.module.modules.misc.ClickGui; - -public final class ModulesMenuScreen { - private static final float INPUT_READY_PROGRESS = 0.72f; - private static final float PANEL_W = 115.0f; - private static final float PANEL_H = 240.0f; - private static final float PANEL_GAP = 14.0f; - private static final float HEADER_H = 24.0f; - private static final float SEPARATOR_H = 4.0f; - private static final float MODULE_ROW_H = 20.0f; - private static final float PANEL_RADIUS = 10.0f; - private static final float TEXT_LEFT_PADDING = 10.0f; - - private static final float LIQUID_GLASS_LOGICAL_SCALE = 3.0f; - private static float ACTIVE_PORT_SCALE = LIQUID_GLASS_LOGICAL_SCALE; - - private final List panels = new ArrayList<>(); - - private float areaX; - private float areaY; - private float areaW; - private float areaH; - private float scale = 1.0f; - private float screenAnim = 0.0f; - private boolean openTarget = false; - - private float searchAnim = 0.0f; - private float searchX; - private float searchY; - private float searchW; - private float searchH; - private float searchBtnX; - private float searchBtnY; - private float searchBtnW; - private float searchBtnH; - private float searchBtnHoverAnim; - - private TextRenderer regular; - private TextRenderer medium; - private TextRenderer semibold; - - public ModulesMenuScreen() { - ModulesMenuCategory[] values = ModulesMenuCategory.values(); - for (int i = 0; i < values.length; i++) { - panels.add(new ModulesMenuPanel(values[i], i)); - } - } - - private static float easeOutCubic(float t) { - t = AnimationUtility.clamp(t, 0.0f, 1.0f); - float inv = 1.0f - t; - return 1.0f - inv * inv * inv; - } - - static float computePortScale() { - return ACTIVE_PORT_SCALE; - } - - private static float computePortScale(float areaW, float areaH) { - float designW = PANEL_W * ModulesMenuCategory.values().length + PANEL_GAP * (ModulesMenuCategory.values().length - 1); - float sideMargin = Math.max(28.0f, areaW * 0.03f); - float verticalReserve = Math.max(56.0f, areaH * 0.10f); - - float fitX = Math.max(1.0f, (areaW - sideMargin * 2.0f) / designW); - float fitY = Math.max(1.0f, (areaH - verticalReserve) / (PANEL_H + 30.0f)); - float fit = Math.min(fitX, fitY); - - return AnimationUtility.clamp(Math.min(LIQUID_GLASS_LOGICAL_SCALE, fit), 2.55f, LIQUID_GLASS_LOGICAL_SCALE); - } - - static boolean isModuleListEditHeld() { - Minecraft client = Minecraft.getInstance(); - if (client == null || client.getWindow() == null || ClickGuiSearch.isActive()) { - return false; - } - return GLFW.glfwGetKey(client.getWindow().handle(), GLFW.GLFW_KEY_E) == GLFW.GLFW_PRESS; - } - - private static float middle(float child, float parent) { - return (parent - child) * 0.5f; - } - - private static float textHeight(TextRenderer renderer, float size) { - return ClickGuiRenderer.textHeight(renderer, size); - } - - static boolean inside(float mx, float my, float x, float y, float w, float h) { - return mx >= x && mx <= x + w && my >= y && my <= y + h; - } - - private static int withAlpha(int color, float alpha) { - float a = AnimationUtility.clamp(alpha, 0.0f, 1.0f); - int ca = (color >>> 24) & 0xFF; - int na = Math.round(ca * a); - return (color & 0x00FFFFFF) | ((na & 0xFF) << 24); - } - - private static int withAlpha(int color, int alpha) { - int a = Math.max(0, Math.min(255, alpha)); - return (color & 0x00FFFFFF) | (a << 24); - } - - private static int mix(int from, int to, float t) { - t = AnimationUtility.clamp(t, 0.0f, 1.0f); - - int a = Math.round(((from >>> 24) & 0xFF) * (1.0f - t) + ((to >>> 24) & 0xFF) * t); - int r = Math.round(((from >>> 16) & 0xFF) * (1.0f - t) + ((to >>> 16) & 0xFF) * t); - int g = Math.round(((from >>> 8) & 0xFF) * (1.0f - t) + ((to >>> 8) & 0xFF) * t); - int b = Math.round((from & 0xFF) * (1.0f - t) + (to & 0xFF) * t); - - return ((a & 0xFF) << 24) | ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (b & 0xFF); - } - - public void open() { - openTarget = true; - } - - public void close() { - openTarget = false; - ClickGuiSearch.deactivate(); - if (!ClickGuiRenderer.isClosingForExit()) { - screenAnim = 0.0f; - resetTransientState(); - } - } - - public boolean isVisible() { - return openTarget || screenAnim > 0.001f; - } - - public void layout(float x, float y, float w, float h) { - this.areaX = x; - this.areaY = y; - this.areaW = Math.max(1.0f, w); - this.areaH = Math.max(1.0f, h); - this.scale = computePortScale(areaW, areaH); - ACTIVE_PORT_SCALE = this.scale; - - float pw = PANEL_W * scale; - float ph = PANEL_H * scale; - float gap = PANEL_GAP * scale; - float total = (pw + gap) * panels.size() - gap; - - float outerPadX = 18.0f * scale; - float outerPadTop = 6.0f * scale; - float outerPadBottom = 26.0f * scale; - - float layoutW = Math.max(total, areaW - outerPadX * 2.0f); - float startX = areaX + (layoutW - total) * 0.5f + (areaW - layoutW) * 0.5f; - startX = Math.max(areaX + outerPadX, startX); - - float py = areaY + outerPadTop + Math.max(0.0f, (areaH - outerPadTop - outerPadBottom - ph) * 0.5f); - for (int i = 0; i < panels.size(); i++) { - ModulesMenuPanel panel = panels.get(i); - float targetX = startX + i * (pw + gap); - float panelT = panelProgress(i); - float eased = easeOutCubic(panelT); - float drop = -(38.0f + i * 3.5f) * scale * (1.0f - eased); - float settle = (float) Math.sin(panelT * Math.PI) * 4.5f * scale; - - panel.x = targetX; - panel.y = py + drop + settle; - panel.w = pw; - panel.h = ph; - panel.anim = panelT; - } - - searchW = 100.0f * scale; - searchH = 20.0f * scale; - searchX = areaX + areaW * 0.5f - searchW * 0.5f; - - float panelBottom = py + ph; - float visibleSearchY = panelBottom + 6.0f * scale; - float maxVisibleSearchY = areaY + areaH - searchH - 8.0f * scale; - if (visibleSearchY > maxVisibleSearchY) { - visibleSearchY = maxVisibleSearchY; - } - - float hiddenSearchY = visibleSearchY + 12.0f * scale; - searchY = AnimationUtility.lerp(hiddenSearchY, visibleSearchY, searchAnim); - - searchBtnW = 22.0f * scale; - searchBtnH = 22.0f * scale; - searchBtnX = areaX + areaW - searchBtnW - 14.0f * scale; - searchBtnY = areaY + 12.0f * scale; - } - - public void render(float mouseX, float mouseY) { - ensureFonts(); - ModulesMenuStyle.syncTheme(); - - float dt = AnimationUtility.deltaTime(); - float target = openTarget ? 1.0f : 0.0f; - - screenAnim = AnimationUtility.approach(screenAnim, target, dt, openTarget ? 4.8f : 7.5f); - screenAnim = AnimationUtility.snap(screenAnim, target, 0.01f); - - if (screenAnim <= 0.001f && !openTarget) { - resetTransientState(); - return; - } - - searchAnim = AnimationUtility.approach(searchAnim, ClickGuiSearch.isActive() ? 1.0f : 0.0f, dt, 10.0f); - searchAnim = AnimationUtility.snap(searchAnim, ClickGuiSearch.isActive() ? 1.0f : 0.0f, 0.01f); - - layout(areaX, areaY, areaW, areaH); - - for (ModulesMenuPanel panel : panels) { - if (panel.isDraggingScrollbar()) panel.updateScrollbarDrag(mouseX, mouseY); - float alpha = panelAlpha(panel); - if (alpha <= 0.001f) continue; - try (RenderWarpStack.Scope ignored = pushPanelDropWarp(panel)) { - renderPanelGlass(panel, alpha); - } - } - - for (ModulesMenuPanel panel : panels) { - float alpha = panelAlpha(panel); - if (alpha <= 0.001f) continue; - try (RenderWarpStack.Scope ignored = pushPanelDropWarp(panel)) { - renderPanel(panel, mouseX, mouseY, alpha); - } - } - - renderSearchButton(mouseX, mouseY); - renderSearch(mouseX, mouseY); - renderHints(); - - ClickGuiRenderer.flushRenderer(); - } - - public void renderGlassPass(float alphaFactor) { - if (screenAnim <= 0.001f) return; - - float passAlpha = AnimationUtility.clamp(alphaFactor, 0.0f, 1.0f); - for (ModulesMenuPanel panel : panels) { - float alpha = panelAlpha(panel) * passAlpha; - if (alpha <= 0.001f) continue; - try (RenderWarpStack.Scope ignored = pushPanelDropWarp(panel)) { - renderPanelGlass(panel, alpha); - } - } - } - - public boolean mousePressed(float mouseX, float mouseY, int button) { - if (!isInteractive()) return false; - - if (button == GLFW.GLFW_MOUSE_BUTTON_LEFT && inside(mouseX, mouseY, searchBtnX, searchBtnY, searchBtnW, searchBtnH)) { - ClickGuiSearch.setActive(!ClickGuiSearch.isActive()); - return true; - } - - if (button == GLFW.GLFW_MOUSE_BUTTON_LEFT && inside(mouseX, mouseY, searchX, searchY, searchW, searchH)) { - ClickGuiSearch.setActive(true); - return true; - } - - for (ModulesMenuPanel panel : panels) { - if (!inside(mouseX, mouseY, panel.x, panel.y, panel.w, panel.h)) continue; - return panel.mousePressed(mouseX, mouseY, button); - } - - if (button == GLFW.GLFW_MOUSE_BUTTON_LEFT && ClickGuiSearch.isActive()) { - ClickGuiSearch.setActive(false); - return true; - } - - return false; - } - - public void mouseReleased(float mouseX, float mouseY, int button) { - if (!isInteractive()) return; - - for (ModulesMenuPanel panel : panels) { - panel.mouseReleased(mouseX, mouseY, button); - } - } - - public boolean mouseScrolled(float mouseX, float mouseY, double amount) { - if (!isInteractive()) return false; - for (ModulesMenuPanel panel : panels) { - if (!inside(mouseX, mouseY, panel.x, panel.y, panel.w, panel.h)) continue; - panel.scroll(mouseX, mouseY, amount); - return true; - } - - return false; - } - - public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - if (!isInteractive()) return false; - if (isHintToggle(keyCode, modifiers)) { - MainConfig config = MainConfig.get(); - config.setClickGuiHintsEnabled(!config.isClickGuiHintsEnabled()); - return true; - } - - boolean ctrl = (modifiers & GLFW.GLFW_MOD_CONTROL) != 0; - - if (ctrl && keyCode == GLFW.GLFW_KEY_F) { - ClickGuiSearch.setActive(!ClickGuiSearch.isActive()); - return true; - } - - if (ClickGuiSearch.isActive()) { - if (keyCode == GLFW.GLFW_KEY_ESCAPE) { - ClickGuiSearch.setActive(false); - return true; - } - - if (keyCode == GLFW.GLFW_KEY_BACKSPACE) { - ClickGuiSearch.backspace(); - return true; - } - } - - for (ModulesMenuPanel panel : panels) { - if (panel.keyPressed(keyCode, scanCode, modifiers)) return true; - } - - return false; - } - - private static boolean isHintToggle(int keyCode, int modifiers) { - return keyCode == GLFW.GLFW_KEY_H && (modifiers & GLFW.GLFW_MOD_ALT) != 0; - } - - public boolean charTyped(char chr, int modifiers) { - if (!isInteractive()) return false; - if (ClickGuiSearch.isActive()) { - ClickGuiSearch.append(chr); - return true; - } - - for (ModulesMenuPanel panel : panels) { - if (panel.charTyped(chr, modifiers)) return true; - } - - return false; - } - - private float panelProgress(int index) { - if (panels.isEmpty()) return screenAnim; - float stagger = 0.055f; - float span = Math.max(0.20f, 1.0f - stagger * Math.max(0, panels.size() - 1)); - return AnimationUtility.clamp((screenAnim - index * stagger) / span, 0.0f, 1.0f); - } - - private float panelAlpha(ModulesMenuPanel panel) { - return easeOutCubic(panel.anim); - } - - private boolean isInteractive() { - return openTarget && screenAnim >= INPUT_READY_PROGRESS; - } - - private RenderWarpStack.Scope pushPanelDropWarp(ModulesMenuPanel panel) { - float t = easeOutCubic(panel.anim); - if (t >= 0.999f) return Renderer2D.pushWarp(null); - float inv = 1.0f - t; - return Renderer2D.pushPerspectiveWarp( - panel.x, - panel.y, - panel.w, - panel.h, - 0.0f, - -9.0f * inv, - 0.0f, - 3.4f, - 0.82f, - 0.94f + 0.06f * t - ); - } - - private void resetTransientState() { - for (ModulesMenuPanel panel : panels) { - panel.selected = null; - panel.selectedTitle = null; - panel.selectedSettings.clear(); - panel.settingsScroll = 0.0f; - panel.settingsSmoothScroll = 0.0f; - panel.modulesScroll = 0.0f; - panel.modulesSmoothScroll = 0.0f; - panel.swap = 0.0f; - panel.hits.clear(); - panel.settingHits.clear(); - } - } - - private void renderPanelGlass(ModulesMenuPanel panel, float alpha) { - if (alpha <= 0.001f) return; - - LayoutRender2D.roundedSoftShadow( - panel.x, - panel.y, - panel.w, - panel.h, - PANEL_RADIUS * scale, - 18.0f * scale, - 0.018f, - withAlpha(ModulesMenuStyle.shadow(), alpha) - ); - - float materialAlpha = alpha >= 0.999f ? 1.0f : alpha; - drawLiquidGlass(panel.x, panel.y, panel.w, panel.h, PANEL_RADIUS * scale, true, materialAlpha); - } - - private void renderPanel(ModulesMenuPanel panel, float mouseX, float mouseY, float alpha) { - panel.update(); - - int text = withAlpha(ModulesMenuStyle.text(), alpha); - int muted = withAlpha(ModulesMenuStyle.textMuted(), alpha); - int split = withAlpha(ModulesMenuStyle.split(), alpha); - int veil = withAlpha(ModulesMenuStyle.panelBgGlassDark(), alpha * 0.30f); - LayoutRender2D.roundedQuad(panel.x, panel.y, panel.w, panel.h, PANEL_RADIUS * scale, veil, veil, veil, veil); - - panel.hits.clear(); - panel.settingHits.clear(); - - float titleY = panel.y + middle(textHeight(semibold, 9.0f * scale), HEADER_H * scale) + 0.5f * scale; - ClickGuiRenderer.drawText(semibold, panel.category.title(), panel.x + TEXT_LEFT_PADDING * scale, titleY, 9.0f * scale, text, false); - drawCategoryIcon(panel, muted); - - if (panel.swap < 0.999f) { - renderModulePage(panel, mouseX, mouseY, alpha * (1.0f - panel.swap)); - } - - if (panel.swap > 0.001f) { - renderSettingsPage(panel, mouseX, mouseY, alpha * panel.swap); - } - - LayoutRender2D.rect( - panel.x + 8.0f * scale, - panel.y + HEADER_H * scale, - panel.w - 16.0f * scale, - 0.5f * scale, - split - ); - } - - private void renderModulePage(ModulesMenuPanel panel, float mouseX, float mouseY, float alpha) { - if (alpha <= 0.001f) return; - - float pageX = panel.x - panel.w * panel.swap; - float listY = panel.y + (HEADER_H + SEPARATOR_H - 1.0f) * scale; - float clipY = panel.y + (HEADER_H + SEPARATOR_H) * scale; - float clipH = panel.h - (HEADER_H + SEPARATOR_H) * scale - 0.5f * scale; - - boolean clipped = ClipFunction.pushRaw(panel.x, clipY, panel.w, clipH); - ClickGuiRenderer.flushRenderer(); - boolean panelShapeClip = ClipFunction.pushRoundedRect(panel.x, panel.y, panel.w, panel.h, PANEL_RADIUS * scale); - float listW = Math.max(1.0f, panel.w - 7.0f * scale); - - float y = listY - panel.modulesSmoothScroll; - float total = 0.0f; - - try { - List entries = ModulesMenuResolver.buildCards(panel.category); - for (ModuleComponent.CardEntry entry : entries) { - if (!ClickGuiSearch.matches(entry.title(), entry.searchAliases())) continue; - - if (y + MODULE_ROW_H * scale >= clipY && y <= clipY + clipH) { - renderModuleRow(panel, entry, pageX, y, listW, mouseX, mouseY, alpha); - } - - y += MODULE_ROW_H * scale; - total += MODULE_ROW_H * scale; - } - } finally { - ClickGuiRenderer.flushRenderer(); - if (panelShapeClip) ClipFunction.pop(); - } - - panel.maxModulesScroll = Math.max(0.0f, total - clipH); - panel.modulesScroll = AnimationUtility.clamp(panel.modulesScroll, 0.0f, panel.maxModulesScroll); - panel.modulesSmoothScroll = AnimationUtility.approach(panel.modulesSmoothScroll, panel.modulesScroll, 0.22f); - panel.modulesSmoothScroll = AnimationUtility.snap(panel.modulesSmoothScroll, panel.modulesScroll, 0.05f); - - if (clipped) ClipFunction.pop(); - - renderScrollbar(panel, panel.modulesSmoothScroll, panel.maxModulesScroll, clipY, clipH, alpha, mouseX, mouseY); - } - - private void renderModuleRow( - ModulesMenuPanel panel, - ModuleComponent.CardEntry entry, - float x, - float y, - float rowW, - float mouseX, - float mouseY, - float alpha - ) { - float h = MODULE_ROW_H * scale; - boolean hover = inside(mouseX, mouseY, x, y, rowW, h); - - float enabled = panel.enabledAnim(entry.getId(), entry.enabled()); - float hoverAnim = panel.hoverAnim(entry.getId(), hover); - - renderModuleRowHover(panel, x, y, rowW, h, mouseX, mouseY, alpha, hoverAnim); - - String label = panel.bindingId != null && panel.bindingId.equals(entry.getId()) - ? bindingLabel(entry) - : entry.title(); - - boolean moduleListEdit = isModuleListEditHeld(); - String moduleListLabel = entry.shownInModuleList() ? "[ON]" : "[OFF]"; - float moduleListTextSize = 7.2f * scale; - float moduleListTextW = moduleListEdit - ? ClickGuiRenderer.textWidth(semibold, moduleListLabel, moduleListTextSize) - : 0.0f; - float moduleListW = moduleListEdit ? moduleListTextW + 3.0f * scale : 0.0f; - float checkReserve = entry.enabled() ? 18.0f * scale : 6.0f * scale; - float moduleListX = x + rowW - checkReserve - moduleListW - 3.0f * scale; - float moduleListY = y + middle(textHeight(semibold, moduleListTextSize), h) - 0.5f * scale; - - int nameColor = mix(withAlpha(ModulesMenuStyle.textMuted(), alpha), withAlpha(ModulesMenuStyle.text(), alpha), 0.25f + 0.75f * enabled + 0.20f * hoverAnim); - - float nameX = x + (TEXT_LEFT_PADDING + 2.0f * enabled) * scale; - float nameY = y + middle(textHeight(regular, 8.0f * scale), h) - 0.5f * scale; - float nameMaxW = moduleListEdit - ? Math.max(12.0f * scale, moduleListX - nameX - 5.0f * scale) - : Math.max(12.0f * scale, rowW - (nameX - x) - 18.0f * scale); - String visibleLabel = ClickGuiRenderer.fitText(regular, label, 8.0f * scale, nameMaxW); - - ClickGuiRenderer.drawText(regular, visibleLabel, nameX, nameY, 8.0f * scale, nameColor, false); - - if (moduleListEdit) { - int listColor = entry.shownInModuleList() ? ModulesMenuStyle.MODULE_LIST_ON : ModulesMenuStyle.MODULE_LIST_OFF; - ClickGuiRenderer.drawText(semibold, moduleListLabel, moduleListX, moduleListY, moduleListTextSize, withAlpha(listColor, alpha), false); - } - - int divider = withAlpha(ModulesMenuStyle.text(), alpha * 0.02f); - LayoutRender2D.rect(x + 3.0f * scale, y + h, Math.max(1f, rowW - 6.0f * scale), 0.5f * scale, divider); - - if (enabled > 0.001f) { - float check = 6.0f * scale; - float checkX = x + rowW - 15.0f * scale - 2.0f * scale * enabled; - float checkY = y + 7.0f * scale; - - Renderer2D.COLOR.svg( - "check", - checkX, - checkY, - check, - check, - SvgRenderOptions.overrideColor(withAlpha(ModulesMenuStyle.text(), alpha * (0.10f + 0.90f * enabled))) - ); - } - - panel.hits.add(new ModulesMenuPanel.ModuleHit(entry.getId(), x, y, rowW, h, entry.hasSettings(), entry.toggleable())); - } - - private void renderModuleRowHover(ModulesMenuPanel panel, - float x, - float y, - float w, - float h, - float mouseX, - float mouseY, - float alpha, - float hoverAnim) { - if (hoverAnim <= 0.001f || alpha <= 0.001f) return; - - float rx = x + 3.0f * scale; - float ry = y + 2.0f * scale; - float rw = w - 6.0f * scale; - float rh = h - 4.0f * scale; - if (rw <= 0.5f || rh <= 0.5f) return; - - float radius = 5.0f * scale; - int hoverBg = withAlpha(ModulesMenuStyle.rowHover(), alpha * hoverAnim); - int glow = ModulesMenuStyle.rowHoverGlow(alpha * hoverAnim); - - float cx = AnimationUtility.clamp(mouseX, rx, rx + rw); - float cy = AnimationUtility.clamp(mouseY, ry, ry + rh); - float glowRadius = Math.max(rw, rh) * 0.82f; - - boolean ownsPanelClip = false; - boolean panelClip = ClipFunction.isShapeClipActive(); - if (!panelClip) { - panelClip = ClipFunction.pushRoundedRect(panel.x, panel.y, panel.w, panel.h, PANEL_RADIUS * scale); - ownsPanelClip = panelClip; - } - if (!panelClip) { - DebugLog.warnOnChange( - "modules.hover.parent.clip.failed", - panel.category.title(), - "ModulesMenuScreen: hover clipped draw skipped because parent rounded glass clip failed. panel=%s panelBounds=[%.2f, %.2f, %.2f, %.2f]", - panel.category.title(), - panel.x, panel.y, panel.w, panel.h - ); - return; - } - - try { - /* - * Only the glass/dropdown shape needs stencil here. - * The hover itself is already a rounded shader primitive and radialGlowMasked() is already - * clipped to the hover rounded rect. A second nested stencil layer for the same hover shape - * makes the packed depth-stencil path dependent on ref=2 and can cull the whole hover draw. - * - * Effective result: - * - rounded hover fill/glow shape is produced by Renderer2D shaders; - * - parent rounded glass clip cuts both of them at the dropdown rounded bottom/edges. - */ - if (((glow >>> 24) & 0xFF) > 0) { - Renderer2D.COLOR.radialGlowMasked(rx, ry, rw, rh, radius, 0.0f, glowRadius, cx, cy, glow); - } - - if (((hoverBg >>> 24) & 0xFF) > 0) { - LayoutRender2D.roundedQuad(rx, ry, rw, rh, radius, hoverBg, hoverBg, hoverBg, hoverBg); - } - } finally { - Renderer2D.flushBatch(Renderer2D.FlushReason.SCISSOR); - if (ownsPanelClip) ClipFunction.pop(); - } - } - - private void renderSettingsPage(ModulesMenuPanel panel, float mouseX, float mouseY, float alpha) { - if (alpha <= 0.001f) return; - - float pageX = panel.x + panel.w * (1.0f - panel.swap); - float settingsPadLeft = 6.0f * scale; - float settingsPadRight = 13.0f * scale; - float settingsX = pageX + settingsPadLeft; - float settingsW = Math.max(1.0f, panel.w - settingsPadLeft - settingsPadRight); - float settingsTop = panel.y + (HEADER_H + SEPARATOR_H) * scale; - float backRowY = panel.y + 28.0f * scale; - float backRowH = 20.0f * scale; - boolean pageClip = ScissorFunction.pushRaw(panel.x, panel.y, panel.w, panel.h); - - boolean headerClip = ScissorFunction.pushRaw(panel.x, panel.y, panel.w, panel.h); - - if (inside(mouseX, mouseY, pageX, backRowY, panel.w, backRowH)) { - int hoverBg = withAlpha(ModulesMenuStyle.rowHover(), alpha); - LayoutRender2D.roundedQuad( - pageX + 3.0f * scale, - backRowY + scale, - panel.w - 6.0f * scale, - backRowH - 2.0f * scale, - 5.0f * scale, - hoverBg, - hoverBg, - hoverBg, - hoverBg - ); - } - - String title = panel.selectedTitle == null ? "Settings" : panel.selectedTitle; - title = ClickGuiRenderer.fitText(regular, title, 8.0f * scale, panel.w - 18.0f * scale); - - ClickGuiRenderer.drawText( - regular, - title, - pageX + TEXT_LEFT_PADDING * scale, - settingsTop + middle(textHeight(regular, 8.0f * scale), HEADER_H * scale) - scale, - 8.0f * scale, - withAlpha(ModulesMenuStyle.text(), alpha), - false - ); - - if (headerClip) ScissorFunction.pop(); - - float clipY = settingsTop + HEADER_H * scale; - float clipH = panel.h - HEADER_H * 2.0f * scale - SEPARATOR_H * scale - 0.5f * scale - 5.0f * scale; - - boolean clipped = ScissorFunction.pushRaw(settingsX, clipY, settingsW, clipH); - - float y = clipY - panel.settingsSmoothScroll; - float total = 0.0f; - - try (SettingRenderContext.Scope ignored = SettingRenderContext.push(SettingRenderSurface.MODULES, scale)) { - for (Setting setting : panel.selectedSettings) { - float vis = setting.updateVisibilityAnim(); - if (!setting.isVisibilityTargetVisible() && vis <= 0.01f) continue; - - float baseH = setting.getHeight(); - float h = baseH * vis; - - if (h > 0.5f) { - panel.settingHits.add(new ModulesMenuPanel.SettingHit(setting, settingsX, y, settingsW, h)); - - if (y + h >= clipY && y <= clipY + clipH) { - float slide = (-baseH + baseH * vis) * 0.5f; - - boolean settingClipped = ScissorFunction.pushRaw(settingsX, y, settingsW, h); - - setting.render(settingsX, y + slide, settingsW, mouseX, mouseY); - - if (settingClipped) ScissorFunction.pop(); - } - } - - y += h; - total += h; - } - } - - panel.maxSettingsScroll = Math.max(0.0f, total - clipH); - panel.settingsScroll = AnimationUtility.clamp(panel.settingsScroll, 0.0f, panel.maxSettingsScroll); - panel.settingsSmoothScroll = AnimationUtility.approach(panel.settingsSmoothScroll, panel.settingsScroll, 0.20f); - panel.settingsSmoothScroll = AnimationUtility.snap(panel.settingsSmoothScroll, panel.settingsScroll, 0.05f); - - if (clipped) ScissorFunction.pop(); - - renderScrollbar(panel, panel.settingsSmoothScroll, panel.maxSettingsScroll, clipY, clipH, alpha, mouseX, mouseY); - if (pageClip) ScissorFunction.pop(); - } - - private void renderSearchButton(float mouseX, float mouseY) { - float alpha = easeOutCubic(screenAnim); - if (alpha <= 0.001f) return; - - boolean hovered = inside(mouseX, mouseY, searchBtnX, searchBtnY, searchBtnW, searchBtnH); - float dt = AnimationUtility.deltaTime(); - searchBtnHoverAnim = AnimationUtility.approach(searchBtnHoverAnim, hovered ? 1.0f : 0.0f, dt, 12.0f); - searchBtnHoverAnim = AnimationUtility.snap(searchBtnHoverAnim, hovered ? 1.0f : 0.0f, 0.01f); - - int bg = withAlpha(ModulesMenuStyle.panelBgGlassDark(), alpha * (0.45f + 0.35f * searchBtnHoverAnim)); - LayoutRender2D.roundedQuad(searchBtnX, searchBtnY, searchBtnW, searchBtnH, 6.0f * scale, bg, bg, bg, bg); - LayoutRender2D.roundedStrokeQuad( - searchBtnX, searchBtnY, searchBtnW, searchBtnH, 6.0f * scale, 0.5f * scale, - withAlpha(ModulesMenuStyle.textMuted(), alpha * 0.35f), - withAlpha(ModulesMenuStyle.textMuted(), alpha * 0.35f), - withAlpha(ModulesMenuStyle.textMuted(), alpha * 0.35f), - withAlpha(ModulesMenuStyle.textMuted(), alpha * 0.35f) - ); - - TextRenderer icons = Fonts.renderer("Icons", FontInfo.Type.Regular, ClickGuiRenderer.getInterRegular()); - float iconSize = 9.0f * scale; - float iconX = searchBtnX + (searchBtnW - ClickGuiRenderer.textWidth(icons, "s", iconSize)) * 0.5f; - float iconY = searchBtnY + middle(textHeight(icons, iconSize), searchBtnH); - int iconColor = withAlpha( - ClickGuiSearch.isActive() ? 0xFFF5F5FF : hovered ? 0xFFD0D0DC : 0xFF878894, - alpha - ); - ClickGuiRenderer.drawText(icons, "s", iconX, iconY, iconSize, iconColor, false); - } - - private void renderSearch(float mouseX, float mouseY) { - if (searchAnim <= 0.001f && !ClickGuiSearch.isActive()) return; - - float alpha = easeOutCubic(screenAnim) * searchAnim; - if (alpha <= 0.001f) return; - - float materialAlpha = alpha >= 0.999f ? 1.0f : alpha; - float radius = 6.0f * scale; - - drawLiquidGlass(searchX, searchY, searchW, searchH, radius, false, materialAlpha); - - int veil = withAlpha(ModulesMenuStyle.panelBgGlassDark(), alpha * 0.25f); - LayoutRender2D.roundedQuad(searchX, searchY, searchW, searchH, radius, veil, veil, veil, veil); - - String text = ClickGuiSearch.getText(); - String draw = text == null || text.isBlank() ? "Search..." : text; - int color = text == null || text.isBlank() ? withAlpha(ModulesMenuStyle.textFaint(), alpha) : withAlpha(ModulesMenuStyle.text(), alpha); - - ClickGuiRenderer.drawText( - regular, - draw, - searchX + 8.0f * scale, - searchY + middle(textHeight(regular, 8.0f * scale), searchH), - 8.0f * scale, - color, - false - ); - } - - private void renderHints() { - MainConfig config = MainConfig.get(); - if (!config.isClickGuiHintsEnabled()) return; - - float alpha = easeOutCubic(screenAnim); - ClickGuiHintOverlay.renderBottomLeft( - areaX, - areaY, - areaW, - areaH, - scale, - alpha, - ClickGuiI18n.tr("clickgui.hints.modules.toggle", "LMB - toggle module"), - ClickGuiI18n.tr("clickgui.hints.modules.settings", "RMB - settings"), - ClickGuiI18n.tr("clickgui.hints.modules.module_list", "E - edit ModuleList"), - ClickGuiI18n.tr("clickgui.hints.modules.search", "Ctrl+F - search"), - ClickGuiI18n.tr("clickgui.hints.modules.close", "Esc - close ClickGui"), - ClickGuiI18n.tr("clickgui.hints.modules.hide", "Alt+H - hide hints") - ); - } - - private void drawLiquidGlass(float x, float y, float w, float h, float radius, boolean panel, float alpha) { - if (alpha <= 0.001f) return; - - float materialAlpha = AnimationUtility.clamp(alpha, 0.0f, 1.0f); - float blurAlpha = AnimationUtility.clamp(materialAlpha * (panel ? 1.20f : 1.04f), 0.0f, 1.0f); - float thickness = (panel ? 15.5f : 12.5f) * scale; - float fresnelPower = panel ? -18.0f : -16.0f; - float fresnelAlpha = 1.0f; - float baseAlpha = panel ? 0.78f : 0.84f; - float fresnelMix = panel ? 0.52f : 0.48f; - float distortion = (panel ? 0.190f : 0.155f) * materialAlpha; - - Renderer2D.COLOR.blurRect( - x, - y, - w, - h, - radius, - ClickGuiRenderer.clickGuiBlurQuality(), - 1.0f, - blurAlpha, - 0xFFFFFF - ); - - Renderer2D.COLOR.liquidGlassRect( - x, - y, - w, - h, - radius, - thickness, - 0xFFFFFFFF, - materialAlpha, - blurAlpha, - fresnelPower, - fresnelAlpha, - baseAlpha, - fresnelMix, - distortion, - 0.0f - ); - } - - private void drawCategoryIcon(ModulesMenuPanel panel, int color) { - float icon = 8.0f * scale; - float ix = panel.x + panel.w - 10.0f * scale - icon; - float iy = panel.y + middle(icon, HEADER_H * scale) + 0.5f * scale; - - Renderer2D.COLOR.svg(panel.category.icon(), ix, iy, icon, icon, SvgRenderOptions.overrideColor(color)); - } - - private void renderScrollbar(ModulesMenuPanel panel, float scroll, float maxScroll, float clipY, float clipH, float alpha, float mouseX, float mouseY) { - boolean settingsPage = panel.selected != null; - if (maxScroll <= 0.5f) { - if (settingsPage) { - panel.settingsScrollbarX = panel.settingsScrollbarY = panel.settingsScrollbarW = panel.settingsScrollbarH = 0f; - } else { - panel.modulesScrollbarX = panel.modulesScrollbarY = panel.modulesScrollbarW = panel.modulesScrollbarH = 0f; - } - return; - } - - float trackW = 2.5f * scale; - float trackX = panel.x + panel.w - 5.0f * scale; - float trackY = clipY + 5.0f * scale; - float trackH = Math.max(1.0f, clipH - 10.0f * scale); - - int trackA = ModulesMenuStyle.scrollTrackA(alpha); - int trackB = ModulesMenuStyle.scrollTrackB(alpha); - int handleA = ModulesMenuStyle.scrollHandleA(alpha); - int handleB = ModulesMenuStyle.scrollHandleB(alpha); - - boolean scrollbarHovered = inside(mouseX, mouseY, trackX - 3.0f * scale, trackY, trackW + 6.0f * scale, trackH); - boolean scrollbarDragging = settingsPage ? panel.settingsScrollbarDragging : panel.modulesScrollbarDragging; - if (scrollbarHovered || scrollbarDragging) { - SystemCursor.set(SystemCursor.CursorType.SCROLL); - } - - LayoutRender2D.roundedQuad( - trackX, - trackY, - trackW, - trackH, - trackW * 0.5f, - trackA, - trackB, - trackB, - trackA - ); - - float handleH = Math.max(18.0f * scale, trackH * (trackH / (trackH + maxScroll))); - float handleY = trackY + (trackH - handleH) * (scroll / Math.max(1.0f, maxScroll)); - if (settingsPage) { - panel.settingsScrollbarX = trackX; - panel.settingsScrollbarY = trackY; - panel.settingsScrollbarW = trackW; - panel.settingsScrollbarH = trackH; - panel.settingsScrollbarHandleY = handleY; - panel.settingsScrollbarHandleH = handleH; - } else { - panel.modulesScrollbarX = trackX; - panel.modulesScrollbarY = trackY; - panel.modulesScrollbarW = trackW; - panel.modulesScrollbarH = trackH; - panel.modulesScrollbarHandleY = handleY; - panel.modulesScrollbarHandleH = handleH; - } - - LayoutRender2D.roundedQuad( - trackX, - handleY, - trackW, - handleH, - trackW * 0.5f, - handleA, - handleB, - handleB, - handleA - ); - } - - private void ensureFonts() { - if (regular == null) - regular = Fonts.renderer("Onest", FontInfo.Type.Regular, ClickGuiRenderer.getInterRegular()); - if (medium == null) medium = Fonts.renderer("OnestMedium", FontInfo.Type.Regular, regular); - if (semibold == null) semibold = Fonts.renderer("OnestBold", FontInfo.Type.Regular, medium); - } - - private String bindingLabel(ModuleComponent.CardEntry entry) { - String pending = ClickGuiRenderer.getPendingBindDisplay(); - if (pending != null) return pending; - - String bind = entry.bindLabel(); - return bind == null || bind.isBlank() ? "Binding..." : "Key: " + bind; - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/ConfigProfilesComponent.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/ConfigProfilesComponent.java deleted file mode 100644 index 348746c..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/ConfigProfilesComponent.java +++ /dev/null @@ -1,799 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.config; - -import dev.loki.lovisual.config.profile.codec.ConfigProfileType; -import dev.loki.lovisual.config.profile.diff.ConfigProfileDiff; -import dev.loki.lovisual.config.profile.service.ConfigProfileApplier; -import dev.loki.lovisual.config.profile.service.ConfigProfileService; -import dev.loki.lovisual.config.profile.service.ConfigProfileStorage; -import dev.loki.lovisual.config.profile.snapshot.ConfigProfileMeta; -import org.lwjgl.glfw.GLFW; -import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; -import dev.loki.lovisual.features.gui.clickgui.tabs.ClickGuiSearch; -import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; -import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.config.ConfigProfileCardComponent.CardHit; -import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.LayoutRender2D; -import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiI18n; -import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiMath; -import dev.loki.lovisual.render.engine.animation.AnimationUtility; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.svg.model.SvgRenderOptions; -import dev.loki.lovisual.render.helpers.clip.ScissorFunction; -import dev.loki.lovisual.render.helpers.util.SystemCursor; -import dev.loki.lovisual.util.logging.DebugLog; - -import java.nio.file.Path; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.config.ConfigDiffPanelComponent; -import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.config.ConfigProfileCardComponent; -import dev.loki.lovisual.features.module.lifecycle.Modules; - -public final class ConfigProfilesComponent { - private static final DateTimeFormatter GENERATED_NAME_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH-mm-ss"); - private static final long STATUS_DURATION_MS = 2400L; - - private final ConfigProfileCardComponent cardComponent = new ConfigProfileCardComponent(); - private final ConfigDiffPanelComponent diffPanelComponent = new ConfigDiffPanelComponent(); - private final List hits = new ArrayList<>(); - - private ConfigProfileType type = ConfigProfileType.MODULES; - private ConfigProfileMeta selected; - private ConfigProfileDiff selectedDiff; - private String diffError; - private String statusMessage; - private long statusUntilMs; - private ConfigProfileType editingType; - private String editingId; - private String editingText = ""; - private Rect editingTitleRect = Rect.ZERO; - - private float scroll; - private float smoothedScroll; - private boolean scrollbarVisible; - private boolean draggingScrollbar; - private float scrollbarX; - private float scrollbarY; - private float scrollbarW; - private float scrollbarH; - private float scrollbarThumbY; - private float scrollbarThumbH; - private float scrollbarMaxScroll; - private float scrollbarDragOffset; - - private Rect modulesPill = Rect.ZERO; - private Rect hudPill = Rect.ZERO; - private Rect themesPill = Rect.ZERO; - private Rect saveButton = Rect.ZERO; - private float modeAnim; - private float modulesHoverAnim; - private float hudHoverAnim; - private float themesHoverAnim; - private float saveHoverAnim; - private float savePulseAnim; - - private static boolean inside(ConfigProfileCardComponent.ActionHit hit, float mx, float my) { - return hit != null && ClickGuiMath.insideRect(mx, my, hit.x(), hit.y(), hit.w(), hit.h()); - } - - public void resetScroll() { - scroll = 0f; - smoothedScroll = 0f; - draggingScrollbar = false; - diffPanelComponent.resetScroll(); - } - - public void render(float menuX, float menuY, float menuW, float menuH, float mx, float my, float scale) { - hits.clear(); - SettingsGuiPalette palette = SettingsGuiPalette.current(); - - float areaX = menuX + 31f * scale; - float areaY = menuY + 33f * scale; - float areaW = menuW - 42f * scale; - float areaH = menuH - 39f * scale; - - renderToolbar(areaX, areaY, areaW, mx, my, scale, palette); - - float listX = areaX; - float listY = areaY + 24f * scale; - float listW = areaW; - float listH = areaH - 24f * scale; - - List entries = filteredProfiles(ConfigProfileService.INSTANCE.list(type)); - renderCards(listX, listY, listW, listH, entries, mx, my, scale, palette); - - if (selectedDiff != null || diffError != null || diffPanelComponent.isActive()) { - diffPanelComponent.render( - menuX + menuW + 24f * scale, - menuY + 4f * scale, - 160f * scale, - Math.min(200f * scale, menuH - 8f * scale), - selected, - selectedDiff, - diffError, - scale, - palette - ); - if (!diffPanelComponent.isOpen() && !diffPanelComponent.isActive()) { - selectedDiff = null; - diffError = null; - } - } - - renderStatus(areaX, areaY, areaW, scale, palette); - } - - public boolean mousePressedScrollbar(float mx, float my, int button) { - if (button != 0) return false; - if (diffPanelComponent.mousePressedScrollbar(mx, my, button)) return true; - if (!isScrollbarHovered(mx, my)) return false; - draggingScrollbar = true; - scrollbarDragOffset = ClickGuiMath.insideRect(mx, my, scrollbarX, scrollbarThumbY, scrollbarW, scrollbarThumbH) - ? my - scrollbarThumbY - : scrollbarThumbH * 0.5f; - scrollToMouse(my); - return true; - } - - public void mouseReleased(int button) { - diffPanelComponent.mouseReleased(button); - if (button == 0) draggingScrollbar = false; - } - - public void scroll(float mx, float my, float menuX, float menuY, float menuW, float menuH, double amount, float scale) { - if (diffPanelComponent.scroll(mx, my, amount)) return; - - float x = menuX + 31f * scale; - float y = menuY + 55f * scale; - float w = menuW - 42f * scale; - float h = menuH - 61f * scale; - if (!ClickGuiMath.insideRect(mx, my, x, y, w, h)) return; - scroll += (float) (amount * 22f); - } - - public boolean click(float mx, float my, int button) { - if (button != 0) return false; - - if (selectedDiff != null || diffError != null) { - ConfigDiffPanelComponent.HeaderAction diffAction = diffPanelComponent.headerAction(mx, my, button); - if (diffAction == ConfigDiffPanelComponent.HeaderAction.CLOSE) { - diffPanelComponent.close(); - return true; - } - if (diffAction == ConfigDiffPanelComponent.HeaderAction.SAVE) { - overwriteSelected(); - return true; - } - } - - if (modulesPill.contains(mx, my)) { - switchType(ConfigProfileType.MODULES); - return true; - } - if (hudPill.contains(mx, my)) { - switchType(ConfigProfileType.HUD); - return true; - } - if (themesPill.contains(mx, my)) { - switchType(ConfigProfileType.THEMES); - return true; - } - if (saveButton.contains(mx, my)) { - savePulseAnim = 1f; - saveCurrent(); - return true; - } - if (isScrollbarHovered(mx, my)) return true; - - for (CardEntryHit entryHit : hits) { - CardHit hit = entryHit.hit(); - if (inside(hit.title(), mx, my)) { - select(entryHit.meta()); - beginRename(entryHit.meta(), hit.title()); - return true; - } - if (inside(hit.diff(), mx, my)) { - select(entryHit.meta()); - diffPanelComponent.resetScroll(); - loadDiff(entryHit.meta()); - return true; - } - if (inside(hit.apply(), mx, my)) { - select(entryHit.meta()); - apply(entryHit.meta()); - return true; - } - if (inside(hit.delete(), mx, my)) { - delete(entryHit.meta()); - return true; - } - if (ClickGuiMath.insideRect(mx, my, hit.x(), hit.y(), hit.w(), hit.h())) { - select(entryHit.meta()); - return true; - } - } - return false; - } - - private void renderToolbar(float x, float y, float w, float mx, float my, float scale, SettingsGuiPalette palette) { - float pillW = 132f * scale; - float pillH = 15f * scale; - float targetMode = switch (type) { - case MODULES -> 0f; - case HUD -> 1f; - case THEMES -> 2f; - }; - float dt = AnimationUtility.deltaTime(); - modeAnim = AnimationUtility.approach(modeAnim, targetMode, dt, 12f); - modeAnim = AnimationUtility.snap(modeAnim, targetMode, 0.01f); - - modulesPill = new Rect(x, y, pillW / 3f, pillH); - hudPill = new Rect(x + pillW / 3f, y, pillW / 3f, pillH); - themesPill = new Rect(x + pillW * 2f / 3f, y, pillW / 3f, pillH); - - modulesHoverAnim = updateHover(modulesHoverAnim, modulesPill.contains(mx, my), dt); - hudHoverAnim = updateHover(hudHoverAnim, hudPill.contains(mx, my), dt); - themesHoverAnim = updateHover(themesHoverAnim, themesPill.contains(mx, my), dt); - - int baseLeft = SettingsGuiPalette.mix(palette.panelPillBase(), palette.panelBgLeft(), 0.34f); - int baseRight = SettingsGuiPalette.mix(palette.panelPillBase(), palette.panelBgRight(), 0.44f); - float anyHover = Math.max(modulesHoverAnim, Math.max(hudHoverAnim, themesHoverAnim)); - baseLeft = SettingsGuiPalette.mix(baseLeft, palette.menuCategoryHoverLeft(), 0.12f * anyHover); - baseRight = SettingsGuiPalette.mix(baseRight, palette.menuCategoryHoverRight(), 0.12f * anyHover); - - ClickGuiRenderer.drawBlur(x, y, pillW, pillH, 4f * scale, palette.panelBlurTint(), 145f / 255f); - LayoutRender2D.roundedQuad( - x, - y, - pillW, - pillH, - 3.5f * scale, - baseLeft, - baseRight, - baseRight, - baseLeft - ); - LayoutRender2D.roundedStrokeQuad( - x, - y, - pillW, - pillH, - 3.5f * scale, - 0.5f * scale, - SettingsGuiPalette.mix(palette.panelStroke(), palette.moduleDividerStart(), 0.18f), - SettingsGuiPalette.mix(palette.panelStroke(), palette.moduleDividerEnd(), 0.18f), - SettingsGuiPalette.mix(palette.panelStroke(), palette.moduleDividerEnd(), 0.18f), - SettingsGuiPalette.mix(palette.panelStroke(), palette.moduleDividerStart(), 0.18f) - ); - - float pad = 1.2f * scale; - float segment = (pillW - pad * 2f) / 3f; - float activeX = x + pad + segment * modeAnim; - float activeHover = switch (type) { - case MODULES -> modulesHoverAnim; - case HUD -> hudHoverAnim; - case THEMES -> themesHoverAnim; - }; - int activeA = SettingsGuiPalette.mix(palette.panelPillActive(), palette.menuCategorySelectedLeft(), 0.22f + activeHover * 0.08f); - int activeB = SettingsGuiPalette.mix(palette.panelPillActive(), palette.menuCategorySelectedRight(), 0.22f + activeHover * 0.12f); - int activeC = SettingsGuiPalette.mix(SettingsGuiPalette.darken(palette.panelPillActive(), 0.16f), palette.menuCategorySelectedRight(), 0.18f + activeHover * 0.10f); - LayoutRender2D.roundedQuad( - activeX, - y + pad, - segment, - pillH - pad * 2f, - 3f * scale, - activeA, - activeB, - activeC, - activeA - ); - LayoutRender2D.roundedStrokeQuad( - activeX, - y + pad, - segment, - pillH - pad * 2f, - 3f * scale, - 0.35f * scale, - SettingsGuiPalette.withAlpha(palette.moduleDividerStart(), 150), - SettingsGuiPalette.withAlpha(palette.moduleDividerEnd(), 178), - SettingsGuiPalette.withAlpha(palette.moduleDividerEnd(), 142), - SettingsGuiPalette.withAlpha(palette.moduleDividerStart(), 150) - ); - - drawToolbarSeparator(x + pad + segment, y, pillH, scale, palette); - drawToolbarSeparator(x + pad + segment * 2f, y, pillH, scale, palette); - - drawSegment(modulesPill, ClickGuiI18n.tr("clickgui.settings.config.type.modules", "Modules"), type == ConfigProfileType.MODULES, modulesHoverAnim, scale, palette); - drawSegment(hudPill, ClickGuiI18n.tr("clickgui.settings.config.type.hud", "Hud"), type == ConfigProfileType.HUD, hudHoverAnim, scale, palette); - drawSegment(themesPill, ClickGuiI18n.tr("clickgui.settings.config.type.themes", "Themes"), type == ConfigProfileType.THEMES, themesHoverAnim, scale, palette); - - float saveW = 68f * scale; - float saveH = pillH; - saveButton = new Rect(x + pillW + 7f * scale, y, saveW, saveH); - boolean saveHover = saveButton.contains(mx, my); - saveHoverAnim = updateHover(saveHoverAnim, saveHover, dt); - savePulseAnim = AnimationUtility.snap(AnimationUtility.approach(savePulseAnim, 0f, dt, 7f), 0f, 0.01f); - renderSaveButton(saveButton, saveHoverAnim, savePulseAnim, scale, palette); - if (saveHover) SystemCursor.set(SystemCursor.CursorType.MOVE); - } - - private float updateHover(float current, boolean hovered, float dt) { - return AnimationUtility.snap( - AnimationUtility.approach(current, hovered ? 1f : 0f, dt, 12f), - hovered ? 1f : 0f, - 0.01f - ); - } - - private void drawToolbarSeparator(float x, float y, float h, float scale, SettingsGuiPalette palette) { - float backingW = Math.max(1.25f * scale, 1.05f); - float backingH = h - 4.4f * scale; - float backingY = y + (h - backingH) * 0.5f; - int backingTop = SettingsGuiPalette.withAlpha(palette.panelBgLeft(), 98); - int backingBottom = SettingsGuiPalette.withAlpha(palette.panelBgRight(), 78); - LayoutRender2D.roundedQuad( - x - backingW * 0.5f, - backingY, - backingW, - backingH, - backingW * 0.5f, - backingTop, - backingTop, - backingBottom, - backingBottom - ); - - float lineW = Math.max(0.5f * scale, 0.75f); - float lineH = h - 6.0f * scale; - float lineY = y + (h - lineH) * 0.5f; - int top = SettingsGuiPalette.withAlpha(SettingsGuiPalette.mix(palette.moduleDividerStart(), palette.menuCategorySelectedLeft(), 0.34f), 146); - int mid = SettingsGuiPalette.withAlpha(SettingsGuiPalette.mix(palette.moduleDividerEnd(), palette.menuCategorySelectedRight(), 0.40f), 210); - int bottom = SettingsGuiPalette.withAlpha(SettingsGuiPalette.mix(palette.moduleDividerEnd(), palette.panelText(), 0.24f), 136); - LayoutRender2D.roundedQuad( - x - lineW * 0.5f, - lineY, - lineW, - lineH, - lineW * 0.5f, - top, - mid, - bottom, - top - ); - - } - - private void renderSaveButton(Rect rect, float hover, float pulse, float scale, SettingsGuiPalette palette) { - float lift = (hover * 0.65f + pulse * 0.85f) * scale; - float x = rect.x(); - float y = rect.y() - lift * 0.15f; - float w = rect.w(); - float h = rect.h(); - int bgA = SettingsGuiPalette.mix(palette.panelPillBase(), palette.menuCategoryHoverLeft(), 0.18f + hover * 0.22f + pulse * 0.24f); - int bgB = SettingsGuiPalette.mix(palette.panelPillBase(), palette.menuCategoryHoverRight(), 0.16f + hover * 0.24f + pulse * 0.26f); - int bgC = SettingsGuiPalette.mix(SettingsGuiPalette.darken(palette.panelPillBase(), 0.18f), palette.menuCategorySelectedRight(), 0.12f + hover * 0.18f + pulse * 0.24f); - LayoutRender2D.roundedSoftShadow( - x, - y + 0.4f * scale, - w, - h, - 3.6f * scale, - 5f * scale, - 0.02f + hover * 0.03f, - SettingsGuiPalette.withAlpha(palette.menuShadow(), Math.round(65f + hover * 50f + pulse * 58f)) - ); - LayoutRender2D.roundedQuad(x, y, w, h, 3.6f * scale, bgA, bgB, bgC, bgA); - LayoutRender2D.roundedStrokeQuad( - x, - y, - w, - h, - 3.6f * scale, - (0.45f + hover * 0.22f + pulse * 0.22f) * scale, - SettingsGuiPalette.withAlpha(palette.moduleDividerStart(), Math.round(138f + hover * 45f + pulse * 50f)), - SettingsGuiPalette.withAlpha(palette.moduleDividerEnd(), Math.round(158f + hover * 55f + pulse * 55f)), - SettingsGuiPalette.withAlpha(palette.moduleDividerEnd(), Math.round(120f + hover * 45f + pulse * 50f)), - SettingsGuiPalette.withAlpha(palette.moduleDividerStart(), Math.round(138f + hover * 45f + pulse * 50f)) - ); - - int iconColor = SettingsGuiPalette.mix(palette.panelText(), palette.menuCategoryText(), 0.22f + hover * 0.34f + pulse * 0.34f); - Renderer2D.COLOR.svg("save", x + 6.2f * scale, y + 4f * scale, 7f * scale, 7f * scale, SvgRenderOptions.overrideColor(iconColor)); - String label = ClickGuiI18n.tr("clickgui.settings.config.save", "Save"); - float textSize = 6.7f * scale; - int textColor = SettingsGuiPalette.mix(palette.panelText(), palette.menuCategoryText(), hover * 0.24f + pulse * 0.24f); - ClickGuiRenderer.drawText(ClickGuiRenderer.getInterMedium(), label, x + 17.4f * scale, y + 4.2f * scale, textSize, textColor, false); - } - - private void drawSegment(Rect rect, String label, boolean active, float hover, float scale, SettingsGuiPalette palette) { - float size = 7.5f * scale; - float tw = ClickGuiRenderer.textWidth(ClickGuiRenderer.getInterMedium(), label, size); - float th = ClickGuiRenderer.textHeight(ClickGuiRenderer.getInterMedium(), size); - int color = active - ? palette.panelText() - : SettingsGuiPalette.mix(palette.panelMuted(), palette.panelText(), 0.20f * hover); - ClickGuiRenderer.drawText(ClickGuiRenderer.getInterMedium(), label, rect.x() + (rect.w() - tw) * 0.5f, rect.y() + (rect.h() - th) * 0.5f, size, color, false); - } - - private void renderCards(float x, - float y, - float w, - float h, - List entries, - float mx, - float my, - float scale, - SettingsGuiPalette palette) { - float gap = 6f * scale; - int columns = w > 230f * scale ? 2 : 1; - float cardW = (w - gap * (columns - 1)) / columns; - float cardH = 58f * scale; - float stepY = cardH + gap; - int rows = (entries.size() + columns - 1) / columns; - float contentH = rows <= 0 ? 0f : rows * stepY - gap; - float maxScroll = Math.max(0f, contentH - h); - updateScrollbarMetrics(x, y, w, h, maxScroll, scale); - if (draggingScrollbar) scrollToMouse(ClickGuiRenderer.getMouseY()); - - scroll = ClickGuiMath.clamp(scroll, -maxScroll, 0f); - smoothedScroll = AnimationUtility.approach(smoothedScroll, scroll, draggingScrollbar ? 0.55f : 0.2f); - smoothedScroll = AnimationUtility.snap(smoothedScroll, scroll, draggingScrollbar ? 0.01f : 0.05f); - - if (entries.isEmpty()) { - String empty = ClickGuiSearch.isActive() && !ClickGuiSearch.getText().isBlank() - ? ClickGuiI18n.tr("clickgui.settings.config.empty.no_matches", "No matching configs.") - : ClickGuiI18n.tr("clickgui.settings.config.empty.none_saved", "No saved configs."); - float emptySize = 8.0f * scale; - float emptyW = ClickGuiRenderer.textWidth(ClickGuiRenderer.getInterRegular(), empty, emptySize); - ClickGuiRenderer.drawText(ClickGuiRenderer.getInterRegular(), empty, x + (w - emptyW) * 0.5f, y + h * 0.42f, emptySize, palette.panelMuted(), false); - if (!ClickGuiSearch.isActive() || ClickGuiSearch.getText().isBlank()) { - String hint = ClickGuiI18n.tr("clickgui.settings.config.empty.drop_hint", "Drop .lvcfg files here, or put them anywhere under config/lovisual/profiles/."); - float hintSize = 6.9f * scale; - String fitted = ClickGuiRenderer.fitText(ClickGuiRenderer.getInterRegular(), hint, hintSize, w - 18f * scale); - float hintW = ClickGuiRenderer.textWidth(ClickGuiRenderer.getInterRegular(), fitted, hintSize); - ClickGuiRenderer.drawText(ClickGuiRenderer.getInterRegular(), fitted, x + (w - hintW) * 0.5f, y + h * 0.42f + 13f * scale, hintSize, palette.panelMuted(), false); - } - return; - } - boolean clipped = ScissorFunction.pushRaw(x, y, w, h); - for (int i = 0; i < entries.size(); i++) { - int col = i % columns; - int row = i / columns; - float cx = x + col * (cardW + gap); - float cy = y + row * stepY + smoothedScroll; - if (cy + cardH < y || cy > y + h) continue; - ConfigProfileMeta meta = entries.get(i); - boolean selectedState = selected != null && selected.getId().equals(meta.getId()) && selected.type() == meta.type(); - CardHit hit = cardComponent.render(meta, cx, cy, cardW, cardH, mx, my, selectedState, scale, palette); - if (isEditing(meta)) { - editingTitleRect = new Rect(hit.title().x(), hit.title().y(), hit.title().w(), hit.title().h()); - renderRenameInput(editingTitleRect, scale, palette); - } - if (inside(hit.title(), mx, my)) SystemCursor.set(SystemCursor.CursorType.MOVE); - hits.add(new CardEntryHit(meta, hit)); - } - if (clipped) ScissorFunction.pop(); - - renderScrollbar(x, y, w, h, maxScroll, scale, palette); - } - - private void renderScrollbar(float x, float y, float w, float h, float maxScroll, float scale, SettingsGuiPalette palette) { - updateScrollbarMetrics(x, y, w, h, maxScroll, scale); - if (!scrollbarVisible) return; - float mx = ClickGuiRenderer.getMouseX(); - float my = ClickGuiRenderer.getMouseY(); - if (isScrollbarHovered(mx, my) || draggingScrollbar) SystemCursor.set(SystemCursor.CursorType.SCROLL); - - LayoutRender2D.roundedQuad(scrollbarX, scrollbarY, scrollbarW, scrollbarH, 2f * scale, - palette.moduleScrollTrackA(), palette.moduleScrollTrackB(), palette.moduleScrollTrackB(), palette.moduleScrollTrackA()); - LayoutRender2D.roundedQuad(scrollbarX, scrollbarThumbY, scrollbarW, scrollbarThumbH, 2f * scale, - palette.moduleScrollHandleA(), palette.moduleScrollHandleB(), palette.moduleScrollHandleB(), palette.moduleScrollHandleA()); - } - - private void updateScrollbarMetrics(float x, float y, float w, float h, float maxScroll, float scale) { - scrollbarVisible = maxScroll > 0.5f; - scrollbarMaxScroll = Math.max(0f, maxScroll); - if (!scrollbarVisible) { - draggingScrollbar = false; - scrollbarX = scrollbarY = scrollbarW = scrollbarH = scrollbarThumbY = scrollbarThumbH = 0f; - return; - } - scrollbarW = 3f * scale; - scrollbarX = x + w - scrollbarW - 2f * scale; - scrollbarY = y + 3f * scale; - scrollbarH = Math.max(1f, h - 6f * scale); - scrollbarThumbH = Math.max(18f * scale, scrollbarH * (scrollbarH / (scrollbarMaxScroll + scrollbarH))); - float scrollRatio = scrollbarMaxScroll <= 0f ? 0f : (-smoothedScroll / scrollbarMaxScroll); - scrollbarThumbY = scrollbarY + (scrollbarH - scrollbarThumbH) * AnimationUtility.clamp(scrollRatio, 0f, 1f); - } - - private boolean isScrollbarHovered(float mx, float my) { - if (!scrollbarVisible) return false; - float pad = 4f; - return ClickGuiMath.insideRect(mx, my, scrollbarX - pad, scrollbarY - pad, scrollbarW + pad * 2f, scrollbarH + pad * 2f); - } - - private void scrollToMouse(float my) { - if (!scrollbarVisible || scrollbarMaxScroll <= 0f) return; - float span = scrollbarH - scrollbarThumbH; - if (span <= 0.5f) return; - float thumbTop = AnimationUtility.clamp(my - scrollbarDragOffset, scrollbarY, scrollbarY + span); - float ratio = (thumbTop - scrollbarY) / span; - scroll = -scrollbarMaxScroll * AnimationUtility.clamp(ratio, 0f, 1f); - } - - public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - if (!isRenaming()) return false; - if (keyCode == GLFW.GLFW_KEY_ESCAPE) { - cancelRename(); - return true; - } - if (keyCode == GLFW.GLFW_KEY_ENTER || keyCode == GLFW.GLFW_KEY_KP_ENTER) { - commitRename(); - return true; - } - if (keyCode == GLFW.GLFW_KEY_BACKSPACE) { - if (!editingText.isEmpty()) editingText = editingText.substring(0, editingText.length() - 1); - return true; - } - return true; - } - - public boolean charTyped(char chr, int modifiers) { - if (!isRenaming()) return false; - if (chr >= 32 && chr != 127 && editingText.length() < 48) { - editingText += chr; - } - return true; - } - - public boolean importDroppedFiles(List paths) { - if (paths == null || paths.isEmpty()) return false; - int imported = 0; - int failed = 0; - ConfigProfileMeta last = null; - for (Path path : paths) { - try { - ConfigProfileStorage.ImportResult result = ConfigProfileService.INSTANCE.importProfileFile(path, false); - imported++; - last = result.meta(); - } catch (Exception e) { - failed++; - DebugLog.error("Failed to import dropped config profile %s", e, path == null ? "" : path.toAbsolutePath()); - } - } - if (last != null) { - switchType(last.type()); - selected = last; - } - if (imported > 0 && failed > 0) { - setStatus(ClickGuiI18n.tr("clickgui.settings.config.status.imported_partial", "Imported: %s, failed: %s", imported, failed)); - } else if (imported > 0) { - setStatus(ClickGuiI18n.tr("clickgui.settings.config.status.imported", "Imported: %s", imported)); - } else { - setStatus(ClickGuiI18n.tr("clickgui.settings.config.status.import_failed", "Import failed")); - } - return true; - } - - private void beginRename(ConfigProfileMeta meta, ConfigProfileCardComponent.ActionHit titleHit) { - if (meta == null) return; - editingType = meta.type(); - editingId = meta.getId(); - editingText = meta.name(); - editingTitleRect = new Rect(titleHit.x(), titleHit.y(), titleHit.w(), titleHit.h()); - clearStatus(); - } - - private void cancelRename() { - editingType = null; - editingId = null; - editingText = ""; - editingTitleRect = Rect.ZERO; - } - - private boolean isRenaming() { - return editingType != null && editingId != null; - } - - private boolean isEditing(ConfigProfileMeta meta) { - return meta != null && editingType == meta.type() && editingId != null && editingId.equals(meta.getId()); - } - - private void commitRename() { - if (!isRenaming()) return; - String next = editingText == null ? "" : editingText.trim(); - if (next.isBlank()) { - setStatus(ClickGuiI18n.tr("clickgui.settings.config.status.empty_name", "Name cannot be empty")); - return; - } - try { - ConfigProfileMeta oldSelected = selected; - ConfigProfileMeta renamed = ConfigProfileService.INSTANCE.rename(editingType, editingId, next).meta(); - if (oldSelected != null && oldSelected.type() == editingType && oldSelected.getId().equals(editingId)) { - selected = renamed; - } - cancelRename(); - setStatus(ClickGuiI18n.tr("clickgui.settings.config.status.renamed", "Renamed: %s", renamed.name())); - } catch (Exception e) { - setStatus(ClickGuiI18n.tr("clickgui.settings.config.status.rename_failed", "Rename failed")); - DebugLog.error("Failed to rename config profile", e); - } - } - - private void renderRenameInput(Rect rect, float scale, SettingsGuiPalette palette) { - float padX = 2f * scale; - float y = rect.y() - scale; - float h = rect.h() + 2f * scale; - LayoutRender2D.roundedQuad( - rect.x() - padX, - y, - rect.w() + padX * 2f, - h, - 3f * scale, - SettingsGuiPalette.mix(palette.moduleCardTop(), palette.menuCategorySelectedLeft(), 0.28f), - SettingsGuiPalette.mix(palette.moduleCardTopStrong(), palette.menuCategorySelectedRight(), 0.22f), - SettingsGuiPalette.mix(palette.moduleCardTopStrong(), palette.menuCategorySelectedRight(), 0.22f), - SettingsGuiPalette.mix(palette.moduleCardTop(), palette.menuCategorySelectedLeft(), 0.28f) - ); - String text = ClickGuiRenderer.fitText(ClickGuiRenderer.getInterMedium(), editingText, 8.8f * scale, rect.w() - 5f * scale); - ClickGuiRenderer.drawText(ClickGuiRenderer.getInterMedium(), text, rect.x(), rect.y(), 8.8f * scale, palette.moduleTitleText(), false); - if ((System.currentTimeMillis() / 500L) % 2L == 0L) { - float tw = ClickGuiRenderer.textWidth(ClickGuiRenderer.getInterMedium(), text, 8.8f * scale); - LayoutRender2D.rect(rect.x() + Math.min(tw + 1.5f * scale, rect.w() - 2f * scale), rect.y() + 1.2f * scale, 0.6f * scale, 8f * scale, palette.moduleTitleText()); - } - } - - private void renderStatus(float x, float y, float w, float scale, SettingsGuiPalette palette) { - if (statusMessage == null || statusMessage.isBlank()) return; - if (System.currentTimeMillis() > statusUntilMs) { - clearStatus(); - return; - } - float size = 6.8f * scale; - String text = ClickGuiRenderer.fitText(ClickGuiRenderer.getInterRegular(), statusMessage, size, w - 176f * scale); - float tw = ClickGuiRenderer.textWidth(ClickGuiRenderer.getInterRegular(), text, size); - ClickGuiRenderer.drawText(ClickGuiRenderer.getInterRegular(), text, x + w - tw, y + 7.2f * scale, size, palette.panelMuted(), false); - } - - private void setStatus(String message) { - statusMessage = message; - statusUntilMs = System.currentTimeMillis() + STATUS_DURATION_MS; - } - - private void clearStatus() { - statusMessage = null; - statusUntilMs = 0L; - } - - private List filteredProfiles(List src) { - if (!ClickGuiSearch.isActive() || ClickGuiSearch.getText().isBlank()) return src; - List out = new ArrayList<>(); - String q = ClickGuiSearch.getText().toLowerCase(Locale.ROOT); - for (ConfigProfileMeta meta : src) { - if (meta.name().toLowerCase(Locale.ROOT).contains(q) || meta.author().toLowerCase(Locale.ROOT).contains(q)) - out.add(meta); - } - return out; - } - - private void switchType(ConfigProfileType next) { - if (type == next) return; - type = next; - selected = null; - selectedDiff = null; - diffError = null; - diffPanelComponent.close(); - clearStatus(); - cancelRename(); - resetScroll(); - } - - private void select(ConfigProfileMeta meta) { - boolean same = selected != null && meta != null && selected.type() == meta.type() && selected.getId().equals(meta.getId()); - selected = meta; - if (!same) { - selectedDiff = null; - diffError = null; - diffPanelComponent.close(); - diffPanelComponent.resetScroll(); - } - } - - private void saveCurrent() { - String name = type.folderName() + " " + GENERATED_NAME_TIME.format(LocalDateTime.now()); - try { - diffPanelComponent.close(); - selected = ConfigProfileService.INSTANCE.saveCurrent(type, name).meta(); - selectedDiff = null; - diffError = null; - setStatus(ClickGuiI18n.tr("clickgui.settings.config.status.saved", "Saved: %s", selected.name())); - } catch (Exception e) { - setStatus(ClickGuiI18n.tr("clickgui.settings.config.status.save_failed", "Save failed")); - DebugLog.error("Failed to save config profile", e); - } - } - - private void overwriteSelected() { - if (selected == null) return; - try { - diffPanelComponent.close(); - selected = ConfigProfileService.INSTANCE.overwriteCurrent(selected).meta(); - selectedDiff = null; - diffError = null; - diffPanelComponent.resetScroll(); - setStatus(ClickGuiI18n.tr("clickgui.settings.config.status.overwritten", "Overwritten: %s", selected.name())); - } catch (Exception e) { - setStatus(ClickGuiI18n.tr("clickgui.settings.config.status.overwrite_failed", "Overwrite failed")); - DebugLog.error("Failed to overwrite config profile", e); - } - } - - private void loadDiff(ConfigProfileMeta meta) { - try { - selectedDiff = ConfigProfileService.INSTANCE.diffWithCurrent(meta.type(), meta.getId()); - diffError = null; - diffPanelComponent.open(); - diffPanelComponent.resetScroll(); - setStatus(selectedDiff.isEmpty() - ? ClickGuiI18n.tr("clickgui.settings.config.status.no_differences", "No differences") - : ClickGuiI18n.tr("clickgui.settings.config.status.diff_count", "Diff: %s changes", selectedDiff.valueChangeCount())); - } catch (Exception e) { - selectedDiff = null; - diffError = e.getMessage(); - diffPanelComponent.open(); - diffPanelComponent.resetScroll(); - setStatus(ClickGuiI18n.tr("clickgui.settings.config.status.diff_failed", "Diff failed")); - DebugLog.error("Failed to diff config profile", e); - } - } - - private void apply(ConfigProfileMeta meta) { - try { - diffPanelComponent.close(); - ConfigProfileApplier.ApplyResult result = ConfigProfileService.INSTANCE.apply(meta.type(), meta.getId()); - selectedDiff = null; - diffError = null; - setStatus(ClickGuiI18n.tr("clickgui.settings.config.status.applied", "Applied: %s, missing: %s/%s", result.appliedOwners(), result.missingOwners(), result.missingValues())); - } catch (Exception e) { - setStatus(ClickGuiI18n.tr("clickgui.settings.config.status.apply_failed", "Apply failed")); - DebugLog.error("Failed to apply config profile", e); - } - } - - private void delete(ConfigProfileMeta meta) { - try { - ConfigProfileService.INSTANCE.delete(meta.type(), meta.getId()); - if (selected != null && selected.getId().equals(meta.getId()) && selected.type() == meta.type()) { - selected = null; - selectedDiff = null; - diffError = null; - diffPanelComponent.close(); - } - setStatus(ClickGuiI18n.tr("clickgui.settings.config.status.deleted", "Deleted: %s", meta.name())); - } catch (Exception e) { - setStatus(ClickGuiI18n.tr("clickgui.settings.config.status.delete_failed", "Delete failed")); - DebugLog.error("Failed to delete config profile", e); - } - } - - private record CardEntryHit(ConfigProfileMeta meta, CardHit hit) { - } - - private record Rect(float x, float y, float w, float h) { - static final Rect ZERO = new Rect(0f, 0f, 0f, 0f); - - boolean contains(float mx, float my) { - return ClickGuiMath.insideRect(mx, my, x, y, w, h); - } - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/OnlineRelationPlayerPickerComponent.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/OnlineRelationPlayerPickerComponent.java deleted file mode 100644 index 8a2cfa0..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/OnlineRelationPlayerPickerComponent.java +++ /dev/null @@ -1,837 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.relations; - -import com.mojang.authlib.GameProfile; -import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; -import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; -import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.LayoutRender2D; -import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiMath; -import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiRichTextRenderer; -import dev.loki.lovisual.features.module.modules.misc.DefineTarget; -import dev.loki.lovisual.features.relations.category.CategoryService; -import dev.loki.lovisual.features.relations.category.CategoryType; -import dev.loki.lovisual.features.relations.PlayerRelations; -import dev.loki.lovisual.render.engine.animation.AnimationUtility; -import dev.loki.lovisual.render.engine.color.RenderColor; -import dev.loki.lovisual.render.engine.core.context.ViewportContext; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.svg.model.SvgRenderOptions; -import dev.loki.lovisual.render.helpers.player.PlayerHeadRenderer; -import dev.loki.lovisual.render.helpers.clip.ScissorFunction; -import dev.loki.lovisual.render.helpers.util.SystemCursor; -import dev.loki.lovisual.util.player.health.PlayerSkinResolver; -import dev.loki.lovisual.util.text.mask.ChatNameUtil; -import dev.loki.lovisual.util.text.format.LegacyTextUtil; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.client.multiplayer.PlayerInfo; -import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiI18n; -import net.minecraft.network.chat.Component; -import net.minecraft.resources.Identifier; -import net.minecraft.world.level.GameType; -import org.lwjgl.glfw.GLFW; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.UUID; -import java.util.function.Consumer; - -/** - * Online player picker for Relations/DefineTarget. - * Visually follows the custom tab-list shape, but writes directly to PlayerRelations. - */ -public final class OnlineRelationPlayerPickerComponent { - private static final int MAX_ROWS_PER_COLUMN = 13; - private static final long STRUCTURE_REFRESH_MS = 350L; - private static final long DYNAMIC_REFRESH_MS = 250L; - private static final String I18N = "clickgui.settings.relations.online_picker."; - - private final Consumer statusSink; - private final List rows = new ArrayList<>(); - private final List rowHits = new ArrayList<>(); - private final Map skinCache = new HashMap<>(); - - private boolean openTarget; - private boolean searchOpen; - private boolean searchFocused; - private String search = ""; - private DefineTarget.RelationTargetMode mode = DefineTarget.RelationTargetMode.FRIEND; - - private Rect bounds = Rect.ZERO; - private Rect searchButton = Rect.ZERO; - private Rect searchField = Rect.ZERO; - private Rect closeButton = Rect.ZERO; - private Rect listRect = Rect.ZERO; - - private float openAnim; - private float searchAnim; - private float scroll; - private float smoothedScroll; - private boolean draggingScrollbar; - private boolean scrollbarVisible; - private float scrollbarX; - private float scrollbarY; - private float scrollbarW; - private float scrollbarH; - private float scrollbarThumbY; - private float scrollbarThumbH; - private float scrollbarMaxScroll; - private float scrollbarDragOffset; - private long lastStructureCheckMs; - private long lastDynamicRefreshMs; - private long lastStructureSignature; - private int lastOnlineCount = -1; - private int rowsVersion; - private int filteredRowsVersion = -1; - private String filteredSearchKey = ""; - private List filteredRowsCache = List.of(); - - OnlineRelationPlayerPickerComponent(Consumer statusSink) { - this.statusSink = statusSink; - } - - boolean isVisible() { - return openTarget || openAnim > 0.002f; - } - - boolean blocksMovementInput() { - return openTarget && searchFocused; - } - - boolean isOpen() { - return openTarget; - } - - boolean isSearchFocused() { - return openTarget && searchFocused; - } - - String searchText() { - return search == null ? "" : search; - } - - void focusSearch() { - if (!openTarget) return; - searchFocused = true; - searchOpen = false; - } - - void clearSearch() { - if (search == null || search.isEmpty()) return; - search = ""; - resetScroll(); - invalidateFilter(); - } - - void open(DefineTarget.RelationTargetMode initialMode) { - openTarget = true; - searchOpen = false; - if (initialMode != null) mode = initialMode; - resetScroll(); - refreshNow(); - } - - void close() { - openTarget = false; - searchFocused = false; - draggingScrollbar = false; - if (!search.isEmpty()) { - search = ""; - invalidateFilter(); - } - } - - void toggle(DefineTarget.RelationTargetMode initialMode) { - if (openTarget) close(); - else open(initialMode); - } - - void resetScroll() { - scroll = 0f; - smoothedScroll = 0f; - draggingScrollbar = false; - } - - void render(float x, float y, float w, float h, float mx, float my, float scale, SettingsGuiPalette palette) { - float dt = AnimationUtility.deltaTime(); - openAnim = AnimationUtility.approach(openAnim, openTarget ? 1.0f : 0.0f, dt, openTarget ? 13.0f : 18.0f); - openAnim = AnimationUtility.snap(openAnim, openTarget ? 1.0f : 0.0f, 0.003f); - searchAnim = AnimationUtility.approach(searchAnim, searchFocused ? 1.0f : 0.0f, dt, 12.0f); - searchAnim = AnimationUtility.snap(searchAnim, searchFocused ? 1.0f : 0.0f, 0.004f); - if (!isVisible()) return; - bounds = new Rect(x, y, w, h); - rowHits.clear(); - refreshIfNeeded(); - - float enter = AnimationUtility.easeOutBack(openAnim, 0.42f); - float exit = AnimationUtility.easeInOutCubic(openAnim); - float visual = openTarget ? enter : exit; - float opacity = openTarget ? AnimationUtility.clamp01(openAnim * 1.16f) : AnimationUtility.smoothstep(openAnim); - float radius = 7.5f * scale; - float lift = openTarget ? (1.0f - visual) * 4.0f * scale : 0.0f; - float panelY = y + lift; - float panelH = openTarget ? h : Math.max(1.0f, h * (0.74f + 0.26f * visual)); - float panelBottom = panelY + panelH; - ClickGuiRenderer.drawBlur(x, panelY, w, panelH, radius, palette.panelBlurTint(), (170f / 255f) * opacity); - LayoutRender2D.roundedSoftShadow(x, panelY + 2f * scale, w, panelH, radius, 15f * scale, 0.18f * opacity, fade(SettingsGuiPalette.withAlpha(0xFF000000, 140), opacity)); - LayoutRender2D.roundedQuad( - x, - panelY, - w, - panelH, - radius, - fade(SettingsGuiPalette.withAlpha(SettingsGuiPalette.mix(palette.panelBgLeft(), palette.menuCategorySelectedLeft(), 0.18f), 216), opacity), - fade(SettingsGuiPalette.withAlpha(SettingsGuiPalette.mix(palette.panelBgRight(), palette.menuCategorySelectedRight(), 0.16f), 208), opacity), - fade(SettingsGuiPalette.withAlpha(SettingsGuiPalette.darken(palette.panelBgRight(), 0.12f), 214), opacity), - fade(SettingsGuiPalette.withAlpha(SettingsGuiPalette.darken(palette.panelBgLeft(), 0.10f), 220), opacity) - ); - Renderer2D.COLOR.radialGlowMasked( - x, - panelY, - w, - panelH, - radius, - 0.0f, - 92f * scale, - x + 42f * scale, - panelY + 30f * scale, - LayoutRender2D.alpha(modeColor(), 0.20f * opacity) - ); - LayoutRender2D.roundedStrokeQuad( - x, - panelY, - w, - panelH, - radius, - 0.65f * scale, - LayoutRender2D.alpha(modeColor(), 0.42f * opacity), - LayoutRender2D.alpha(palette.menuWindowStroke(), 0.78f * opacity), - LayoutRender2D.alpha(palette.menuWindowStroke(), 0.50f * opacity), - LayoutRender2D.alpha(modeColor(), 0.26f * opacity) - ); - - float pad = 8f * scale; - float headerH = 28f * scale; - renderHeader(x + pad, panelY + pad, w - pad * 2f, headerH - pad, mx, my, scale, palette, opacity); - - float listX = x + pad; - float listY = panelY + headerH + 4f * scale; - float listW = w - pad * 2f; - float listH = Math.max(1f, panelBottom - pad - listY); - listRect = new Rect(listX, listY, listW, listH); - renderRows(listX, listY, listW, listH, mx, my, scale, palette, opacity); - } - - private void renderHeader(float x, float y, float w, float h, float mx, float my, float scale, SettingsGuiPalette palette, float opacity) { - float titleSize = 8.2f * scale; - String title = tr("title", "Online players"); - String subtitle = search == null || search.isBlank() - ? tr("subtitle", "Pick from current tab list") - : tr("subtitle_search", "%s / %s visible", filteredRows().size(), rows.size()); - ClickGuiRenderer.drawText(ClickGuiRenderer.getInterMedium(), title, x, y + 1.5f * scale, titleSize, fade(palette.panelText(), opacity), false); - ClickGuiRenderer.drawText(ClickGuiRenderer.getInterRegular(), subtitle, x, y + 13.0f * scale, 6.15f * scale, fade(palette.panelMuted(), opacity), false); - - float btn = 17f * scale; - closeButton = new Rect(x + w - btn, y, btn, btn); - searchButton = Rect.ZERO; - searchField = Rect.ZERO; - drawSmallIconButton(closeButton, "x", false, closeButton.contains(mx, my), scale, palette, opacity); - } - - private void renderRows(float x, float y, float w, float h, float mx, float my, float scale, SettingsGuiPalette palette, float opacity) { - List filtered = filteredRows(); - if (filtered.isEmpty()) { - String text = search.isBlank() ? tr("empty", "No online players.") : tr("empty_search", "No matching online players."); - ClickGuiRenderer.drawText(ClickGuiRenderer.getInterRegular(), text, x + 5f * scale, y + 5f * scale, 7.2f * scale, fade(palette.panelMuted(), opacity), false); - updateScrollbarMetrics(x, y, w, h, 0f, scale); - return; - } - - float reservedScrollbarW = 9f * scale; - int rowsPerColumn = filtered.size(); - float gap = 5f * scale; - float rowH = 31f * scale; - float stepY = rowH + gap; - float contentH = rowsPerColumn * stepY - gap; - float maxScroll = Math.max(0f, contentH - h); - updateScrollbarMetrics(x, y, w, h, maxScroll, scale); - float rowAreaW = scrollbarVisible ? Math.max(1f, w - reservedScrollbarW) : w; - float colW = rowAreaW; - if (draggingScrollbar) scrollToMouse(ClickGuiRenderer.getMouseY()); - - scroll = ClickGuiMath.clamp(scroll, -maxScroll, 0f); - smoothedScroll = AnimationUtility.approach(smoothedScroll, scroll, draggingScrollbar ? 0.55f : 0.22f); - smoothedScroll = AnimationUtility.snap(smoothedScroll, scroll, draggingScrollbar ? 0.01f : 0.05f); - - boolean clipped = ScissorFunction.pushRaw(x, y, w, h); - for (int i = 0; i < filtered.size(); i++) { - int row = i; - float rowX = x; - float rowY = y + row * stepY + smoothedScroll; - if (rowY + rowH < y || rowY > y + h) continue; - PlayerRow player = filtered.get(i); - renderRow(player, rowX, rowY, colW, rowH, mx, my, scale, palette, i, opacity); - } - if (clipped) ScissorFunction.pop(); - renderScrollbar(x, y, w, h, maxScroll, scale, palette, opacity); - } - - private void renderRow(PlayerRow player, float x, float y, float w, float h, float mx, float my, float scale, SettingsGuiPalette palette, int index, float opacity) { - float rowBase = openTarget ? AnimationUtility.easeOutCubic(openAnim) : 1.0f; - float rowAnim = AnimationUtility.clamp((rowBase - index * 0.018f) / 0.72f, 0f, 1f); - float rowEase = openTarget ? AnimationUtility.easeOutCubic(rowAnim) : 1.0f; - y += (1f - rowEase) * 7f * scale; - boolean hover = openTarget && ClickGuiMath.insideRect(mx, my, x, y, w, h); - boolean selected = DefineTarget.isPlayerInRelationMode(mode, player.name()); - int relationColor = player.relationColor() != 0 ? player.relationColor() : modeColor(); - int top = hover ? SettingsGuiPalette.mix(palette.moduleCardTop(), modeColor(), 0.10f) : palette.moduleCardTop(); - int topStrong = hover || selected ? SettingsGuiPalette.mix(palette.moduleCardTopStrong(), relationColor, selected ? 0.18f : 0.10f) : palette.moduleCardTopStrong(); - int bottom = hover ? SettingsGuiPalette.mix(palette.moduleCardBottom(), modeColor(), 0.12f) : palette.moduleCardBottom(); - int bottomStrong = selected ? SettingsGuiPalette.mix(palette.moduleCardBottomStrong(), relationColor, 0.20f) : palette.moduleCardBottomStrong(); - - LayoutRender2D.roundedSoftShadow(x, y + 1f * scale, w, h, 5.5f * scale, 8f * scale, 0.08f, fade(SettingsGuiPalette.withAlpha(0xFF000000, hover ? 96 : 58), opacity)); - LayoutRender2D.roundedQuad(x, y, w, h, 5.5f * scale, fade(top, opacity), fade(topStrong, opacity), fade(bottomStrong, opacity), fade(bottom, opacity)); - Renderer2D.COLOR.radialGlowMasked(x, y, w, h, 5.5f * scale, 0f, 42f * scale, x + 15f * scale, y + h * 0.50f, LayoutRender2D.alpha(relationColor, (selected ? 0.28f : 0.14f) * opacity)); - LayoutRender2D.roundedStrokeQuad(x, y, w, h, 5.5f * scale, 0.55f * scale, - LayoutRender2D.alpha(relationColor, (selected ? 0.70f : 0.34f) * opacity), - LayoutRender2D.alpha(palette.menuWindowStroke(), 0.72f * opacity), - LayoutRender2D.alpha(palette.menuWindowStroke(), 0.44f * opacity), - LayoutRender2D.alpha(relationColor, (selected ? 0.46f : 0.20f) * opacity)); - - float head = 18.5f * scale; - float headX = x + 6f * scale; - float headY = y + (h - head) * 0.5f; - renderHead(resolveSkin(player), headX, headY, head, scale, palette, opacity); - - float dot = 5.3f * scale; - float dotX = headX + head - dot * 0.72f; - float dotY = headY + head - dot * 0.72f; - Renderer2D.COLOR.radialGlowMasked(dotX - 4f * scale, dotY - 4f * scale, dot + 8f * scale, dot + 8f * scale, - (dot + 8f * scale) * 0.5f, 0f, 11f * scale, dotX + dot * 0.5f, dotY + dot * 0.5f, LayoutRender2D.alpha(relationColor, 0.48f * opacity)); - Renderer2D.COLOR.roundedRect(dotX, dotY, dot, dot, dot * 0.5f, 0.7f, fade(relationColor | 0xFF000000, opacity)); - Renderer2D.COLOR.roundedRectStroke(dotX, dotY, dot, dot, dot * 0.5f, 0.7f, 0.55f * scale, LayoutRender2D.alpha(0xFFFFFFFF, 0.42f * opacity)); - - float textX = headX + head + 7f * scale; - float rightPad = selected ? 56f * scale : 44f * scale; - float pingW = Math.max(38f * scale, ClickGuiRenderer.textWidth(ClickGuiRenderer.getInterRegular(), pingText(player.latency()), 6.2f * scale) + 26f * scale); - float nameW = Math.max(20f * scale, w - (textX - x) - rightPad - pingW); - int nameColor = player.spectator() ? palette.panelMuted() : palette.panelText(); - ClickGuiRichTextRenderer.draw(player.displayName(), textX, y + 6.0f * scale, nameW, 8.0f * scale, nameColor, opacity, false); - - String relation = relationLabel(player.relation()); - String relationText = ClickGuiRenderer.fitText(ClickGuiRenderer.getInterRegular(), relation, 5.5f * scale, nameW); - ClickGuiRenderer.drawText(ClickGuiRenderer.getInterRegular(), relationText, textX, y + 18.6f * scale, 5.5f * scale, - fade(SettingsGuiPalette.mix(palette.panelMuted(), relationColor | 0xFF000000, 0.45f), opacity), false); - - float pingChipH = 13f * scale; - float pingChipX = x + w - 6f * scale - pingW; - float pingChipY = y + (h - pingChipH) * 0.5f; - drawPingChip(player.latency(), pingChipX, pingChipY, pingW, pingChipH, scale, palette, opacity); - - Rect hit = new Rect(x, y, w, h); - rowHits.add(new RowHit(player.name(), hit)); - if (hover) SystemCursor.set(SystemCursor.CursorType.HAND); - } - - boolean click(float mx, float my, int button) { - if (!openTarget || button != 0) return false; - if (closeButton.contains(mx, my)) { - close(); - return true; - } - if (searchButton.contains(mx, my)) { - focusSearch(); - return true; - } - if (searchField.contains(mx, my)) { - focusSearch(); - return true; - } - for (RowHit hit : rowHits) { - if (!hit.rect().contains(mx, my)) continue; - addPlayer(hit.name()); - return true; - } - if (bounds.contains(mx, my)) { - searchFocused = false; - return true; - } - return false; - } - - boolean mousePressedScrollbar(float mx, float my, int button) { - if (!openTarget || button != 0 || !isScrollbarHovered(mx, my)) return false; - draggingScrollbar = true; - scrollbarDragOffset = ClickGuiMath.insideRect(mx, my, scrollbarX, scrollbarThumbY, scrollbarW, scrollbarThumbH) - ? my - scrollbarThumbY - : scrollbarThumbH * 0.5f; - scrollToMouse(my); - return true; - } - - void mouseReleased(int button) { - if (button == 0) draggingScrollbar = false; - } - - void scroll(float mx, float my, double amount) { - if (!openTarget || !listRect.contains(mx, my)) return; - scroll += (float) (amount * 24f); - } - - boolean keyPressed(int keyCode, int scanCode, int modifiers) { - if (!openTarget) return false; - boolean ctrl = (modifiers & GLFW.GLFW_MOD_CONTROL) != 0; - if (ctrl && keyCode == GLFW.GLFW_KEY_F) { - focusSearch(); - return true; - } - if (keyCode == GLFW.GLFW_KEY_ESCAPE) { - if (searchFocused) { - searchFocused = false; - return true; - } - close(); - return true; - } - if (!searchFocused) return false; - if (keyCode == GLFW.GLFW_KEY_ENTER || keyCode == GLFW.GLFW_KEY_KP_ENTER) { - searchFocused = false; - return true; - } - if (keyCode == GLFW.GLFW_KEY_BACKSPACE) { - if (!search.isEmpty()) { - search = search.substring(0, search.length() - 1); - resetScroll(); - invalidateFilter(); - } - return true; - } - return true; - } - - boolean charTyped(char chr, int modifiers) { - if (!openTarget || !searchFocused) return false; - if (chr >= 32 && chr != 127) { - search += chr; - if (search.length() > 32) search = search.substring(0, 32); - resetScroll(); - invalidateFilter(); - } - return true; - } - - private void addPlayer(String rawName) { - String name = ChatNameUtil.normalizeNickCandidate(rawName); - if (!ChatNameUtil.isNickLike(name)) { - setStatus(tr("invalid", "Invalid nick")); - return; - } - String modeName = modeDisplayName(mode); - boolean wasInMode = DefineTarget.isPlayerInRelationMode(mode, name); - boolean added = DefineTarget.togglePlayerRelation(mode, name); - refreshNow(); - if (wasInMode && !added) { - setStatus(tr("removed", "Removed %s from %s", name, modeName)); - } else { - setStatus(tr("added", "Added %s as %s", name, modeName)); - } - } - - private void refreshIfNeeded() { - Minecraft mc = Minecraft.getInstance(); - if (mc == null || mc.getConnection() == null) return; - long now = System.currentTimeMillis(); - int onlineCount = onlineCount(); - boolean checkStructure = rows.isEmpty() || onlineCount != lastOnlineCount || now - lastStructureCheckMs >= STRUCTURE_REFRESH_MS; - if (checkStructure) { - long signature = structureSignature(); - lastStructureCheckMs = now; - if (rows.isEmpty() || onlineCount != lastOnlineCount || signature != lastStructureSignature) { - collectRows(); - lastOnlineCount = onlineCount; - lastStructureSignature = signature; - lastDynamicRefreshMs = now; - return; - } - } - - if (now - lastDynamicRefreshMs >= DYNAMIC_REFRESH_MS) { - refreshDynamicRows(); - lastDynamicRefreshMs = now; - } - } - - private void refreshNow() { - collectRows(); - lastOnlineCount = onlineCount(); - lastStructureSignature = structureSignature(); - lastStructureCheckMs = System.currentTimeMillis(); - lastDynamicRefreshMs = lastStructureCheckMs; - } - - private int onlineCount() { - Minecraft mc = Minecraft.getInstance(); - return mc != null && mc.getConnection() != null ? mc.getConnection().getListedOnlinePlayers().size() : 0; - } - - private void collectRows() { - Minecraft mc = Minecraft.getInstance(); - rows.clear(); - if (mc == null || mc.getConnection() == null) { - rowsVersion++; - invalidateFilter(); - return; - } - Collection online = mc.getConnection().getListedOnlinePlayers(); - List sorted = new ArrayList<>(online); - sorted.sort(Comparator - .comparingInt((PlayerInfo info) -> -info.getTabListOrder()) - .thenComparing(info -> info.getGameMode() == GameType.SPECTATOR) - .thenComparing(info -> teamName(info).toLowerCase(Locale.ROOT)) - .thenComparing(info -> profileName(info).toLowerCase(Locale.ROOT), String.CASE_INSENSITIVE_ORDER)); - - Set liveIds = new HashSet<>(); - for (PlayerInfo info : sorted) { - if (info == null || info.getProfile() == null) continue; - GameProfile profile = info.getProfile(); - UUID id = profile.id(); - String name = profile.name(); - if (id == null || name == null || name.isBlank()) continue; - Component display = displayName(info, name); - CategoryType relation = CategoryService.get(name); - int relationColor = relation == CategoryType.DEFAULT ? PlayerRelations.get().colorDefault() : CategoryService.getColor(name); - rows.add(new PlayerRow(id, profile, name, display, searchText(name, display), relation, relationColor, info.getLatency(), info.getGameMode() == GameType.SPECTATOR)); - liveIds.add(id); - } - skinCache.keySet().retainAll(liveIds); - rowsVersion++; - invalidateFilter(); - } - - private void refreshDynamicRows() { - Minecraft mc = Minecraft.getInstance(); - if (mc == null || mc.getConnection() == null || rows.isEmpty()) return; - boolean changed = false; - for (int i = 0; i < rows.size(); i++) { - PlayerRow row = rows.get(i); - PlayerInfo info = mc.getConnection().getPlayerInfo(row.id()); - if (info == null || info.getProfile() == null) { - collectRows(); - return; - } - CategoryType relation = CategoryService.get(row.name()); - int relationColor = relation == CategoryType.DEFAULT ? PlayerRelations.get().colorDefault() : CategoryService.getColor(row.name()); - int latency = info.getLatency(); - boolean spectator = info.getGameMode() == GameType.SPECTATOR; - if (latency == row.latency() && spectator == row.spectator() && relation == row.relation() && relationColor == row.relationColor()) { - continue; - } - rows.set(i, new PlayerRow(row.id(), row.profile(), row.name(), row.displayName(), row.searchText(), relation, relationColor, latency, spectator)); - changed = true; - } - if (changed) { - rowsVersion++; - invalidateFilter(); - } - } - - private long structureSignature() { - Minecraft mc = Minecraft.getInstance(); - if (mc == null || mc.getConnection() == null) return 0L; - long h = 0x6A09E667F3BCC909L; - for (PlayerInfo info : mc.getConnection().getListedOnlinePlayers()) { - if (info == null || info.getProfile() == null) continue; - GameProfile profile = info.getProfile(); - h = mix(h, profile.id()); - h = mix(h, profile.name()); - h = mix(h, info.getTabListOrder()); - h = mix(h, info.getGameMode() == GameType.SPECTATOR ? 1 : 0); - h = mix(h, teamName(info)); - Component rawDisplay = info.getTabListDisplayName(); - h = mix(h, rawDisplay != null ? rawDisplay.getString() : ""); - String name = profile.name() == null ? "" : profile.name(); - CategoryType relation = CategoryService.get(name); - h = mix(h, relation.ordinal()); - h = mix(h, relation == CategoryType.DEFAULT ? PlayerRelations.get().colorDefault() : CategoryService.getColor(name)); - } - return h; - } - - private Component displayName(PlayerInfo info, String fallback) { - Minecraft mc = Minecraft.getInstance(); - Component display = null; - if (mc != null && mc.gui != null && mc.gui.hud != null && mc.gui.hud.getTabList() != null) { - display = mc.gui.hud.getTabList().getNameForDisplay(info); - } - if (display == null) display = info.getTabListDisplayName(); - if (display == null || display.getString().isBlank()) display = Component.literal(fallback); - return LegacyTextUtil.convertLegacyCodes(display); - } - - private List filteredRows() { - String q = normalizedSearch(); - if (filteredRowsVersion == rowsVersion && filteredSearchKey.equals(q)) return filteredRowsCache; - if (q.isBlank()) { - filteredRowsCache = rows; - } else { - List out = new ArrayList<>(); - for (PlayerRow row : rows) { - if (row.searchText().contains(q)) out.add(row); - } - filteredRowsCache = out; - } - filteredRowsVersion = rowsVersion; - filteredSearchKey = q; - return filteredRowsCache; - } - - private String normalizedSearch() { - return search == null ? "" : LegacyTextUtil.stripLegacy(search).trim().toLowerCase(Locale.ROOT); - } - - private void invalidateFilter() { - filteredRowsVersion = -1; - } - - private Identifier resolveSkin(PlayerRow player) { - if (player == null || player.id() == null) return null; - Identifier cached = skinCache.get(player.id()); - if (cached != null) return cached; - Identifier skin = PlayerSkinResolver.resolveProfileSkin(player.profile()); - if (skin != null) skinCache.put(player.id(), skin); - return skin; - } - - private static String searchText(String name, Component displayName) { - String raw = (name == null ? "" : name) + " " + (displayName == null ? "" : displayName.getString()); - return LegacyTextUtil.stripLegacy(raw).trim().toLowerCase(Locale.ROOT); - } - - private static long mix(long h, int value) { - return (h ^ value) * 0x100000001B3L; - } - - private static long mix(long h, UUID value) { - if (value == null) return mix(h, 0); - h = (h ^ value.getMostSignificantBits()) * 0x100000001B3L; - return (h ^ value.getLeastSignificantBits()) * 0x100000001B3L; - } - - private static long mix(long h, String value) { - return (h ^ (value != null ? value.hashCode() : 0)) * 0x100000001B3L; - } - - private void drawPingChip(int latency, float x, float y, float w, float h, float scale, SettingsGuiPalette palette, float opacity) { - String text = pingText(latency); - int base = pingBaseColor(latency); - int chipA = LayoutRender2D.alpha(palette.panelPillBase(), 0.84f * opacity); - int chipB = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelPillBase(), palette.moduleCardBottomStrong(), 0.38f), 0.52f * opacity); - LayoutRender2D.roundedQuad(x, y, w, h, 5.4f * scale, chipA, chipA, chipB, chipB); - LayoutRender2D.roundedStrokeQuad(x, y, w, h, 5.4f * scale, 0.55f * scale, - LayoutRender2D.alpha(palette.panelStroke(), 0.40f * opacity), - LayoutRender2D.alpha(palette.moduleDividerEnd(), 0.26f * opacity), - LayoutRender2D.alpha(palette.moduleDividerEnd(), 0.18f * opacity), - LayoutRender2D.alpha(palette.panelStroke(), 0.30f * opacity)); - - float cx = x + 7.3f * scale; - float cy = y + h * 0.5f; - float r = 3.1f * scale; - Renderer2D.COLOR.circleSoftShadow(cx, cy, r + 1.5f * scale, 5.0f * scale, 0.34f, LayoutRender2D.alpha(base, 0.48f * opacity)); - Renderer2D.COLOR.circle(cx, cy, r + 0.95f * scale, 0.8f * scale, LayoutRender2D.alpha(base, 0.30f * opacity)); - Renderer2D.COLOR.circle(cx, cy, r, 0.75f * scale, LayoutRender2D.alpha(base, opacity)); - Renderer2D.COLOR.circleStroke(cx, cy, r + 0.72f * scale, 0.55f * scale, 0.8f * scale, LayoutRender2D.alpha(0xFFFFFFFF, 0.36f * opacity)); - Renderer2D.COLOR.arcStrokeGradient(cx, cy, r + 1.55f * scale, 0.9f * scale, -140f, 70f, 0.45f * scale, - LayoutRender2D.alpha(base, 0.68f * opacity), - LayoutRender2D.alpha(0xFFFFFFFF, 0.16f * opacity), - 35f, 0f); - - ClickGuiRenderer.drawText(ClickGuiRenderer.getInterMedium(), text, x + 14.5f * scale, y + 3.3f * scale, 6.2f * scale, fade(palette.panelText(), opacity), false); - } - - private void drawSmallIconButton(Rect rect, String icon, boolean active, boolean hover, float scale, SettingsGuiPalette palette, float opacity) { - int color = active ? modeColor() : palette.panelMuted(); - int bgA = active || hover ? SettingsGuiPalette.mix(palette.panelPillBase(), color, active ? 0.30f : 0.14f) : LayoutRender2D.alpha(palette.panelPillBase(), 0.82f); - int bgB = active || hover ? SettingsGuiPalette.mix(palette.moduleCardTopStrong(), color, active ? 0.18f : 0.08f) : LayoutRender2D.alpha(palette.panelPillBase(), 0.70f); - LayoutRender2D.roundedQuad(rect.x(), rect.y(), rect.w(), rect.h(), 4.3f * scale, fade(bgA, opacity), fade(bgB, opacity), fade(bgB, opacity), fade(bgA, opacity)); - LayoutRender2D.roundedStroke(rect.x(), rect.y(), rect.w(), rect.h(), 4.3f * scale, 0.45f * scale, LayoutRender2D.alpha(color, (active || hover ? 0.56f : 0.24f) * opacity)); - Renderer2D.COLOR.svg(icon, rect.x() + 4.1f * scale, rect.y() + 4.1f * scale, rect.w() - 8.2f * scale, rect.h() - 8.2f * scale, - SvgRenderOptions.overrideColor(fade(active ? palette.panelText() : SettingsGuiPalette.mix(palette.panelMuted(), palette.panelText(), hover ? 0.25f : 0.0f), opacity))); - if (hover) SystemCursor.set(SystemCursor.CursorType.HAND); - } - - private void drawSearchField(Rect rect, float mx, float my, float scale, SettingsGuiPalette palette, float opacity) { - boolean hover = rect.contains(mx, my); - int stroke = searchFocused ? modeColor() : palette.panelStroke(); - ClickGuiRenderer.drawBlur(rect.x(), rect.y(), rect.w(), rect.h(), 4.8f * scale, palette.panelBlurTint(), (100f / 255f) * opacity); - LayoutRender2D.roundedQuad(rect.x(), rect.y(), rect.w(), rect.h(), 4.8f * scale, - LayoutRender2D.alpha(palette.panelPillBase(), (hover ? 1.0f : 0.84f) * opacity), - LayoutRender2D.alpha(palette.panelPillBase(), (hover ? 0.92f : 0.76f) * opacity), - LayoutRender2D.alpha(palette.panelPillBase(), (hover ? 0.82f : 0.68f) * opacity), - LayoutRender2D.alpha(palette.panelPillBase(), (hover ? 0.96f : 0.76f) * opacity)); - LayoutRender2D.roundedStroke(rect.x(), rect.y(), rect.w(), rect.h(), 4.8f * scale, 0.55f * scale, LayoutRender2D.alpha(stroke, (searchFocused ? 0.74f : 0.42f) * opacity)); - boolean clipped = ScissorFunction.pushRaw(rect.x(), rect.y(), rect.w(), rect.h()); - String text = search == null || search.isEmpty() ? tr("search_placeholder", "Search online player") : search; - int color = search == null || search.isEmpty() ? palette.panelMuted() : palette.panelText(); - ClickGuiRenderer.drawText(ClickGuiRenderer.getInterRegular(), text, rect.x() + 7f * scale, rect.y() + 5.0f * scale, 6.8f * scale, fade(color, opacity), false); - if (searchFocused && ((System.currentTimeMillis() / 520L) & 1L) == 0L && rect.h() > 10f * scale) { - float tx = rect.x() + 7f * scale + ClickGuiRenderer.textWidth(ClickGuiRenderer.getInterRegular(), search, 6.8f * scale) + 1.0f * scale; - LayoutRender2D.rect(tx, rect.y() + 4.6f * scale, 0.8f * scale, Math.max(0.5f * scale, rect.h() - 9.2f * scale), LayoutRender2D.alpha(modeColor(), 0.86f * opacity)); - } - if (clipped) ScissorFunction.pop(); - if (hover || searchFocused) SystemCursor.set(SystemCursor.CursorType.TEXT); - } - - private void renderHead(Identifier skin, float x, float y, float size, float scale, SettingsGuiPalette palette, float opacity) { - float headOpacity = AnimationUtility.clamp01(opacity * ClickGuiRenderer.getRenderAlphaMultiplier()); - if (headOpacity <= 0.01f) return; - GuiGraphicsExtractor ctx = ViewportContext.getCurrentContext(); - if (skin != null && ctx != null) { - int alpha = Math.round(255f * headOpacity); - PlayerHeadRenderer.drawRounded(ctx, x, y, size, 4.2f * scale, skin, new RenderColor(255, 255, 255, alpha), true, - new RenderColor(255, 255, 255, Math.round(48f * headOpacity)), 0.95f * scale, false); - return; - } - Renderer2D.COLOR.roundedRect(x, y, size, size, 4.2f * scale, 1.0f, LayoutRender2D.alpha(palette.panelMuted(), 0.22f * headOpacity)); - Renderer2D.COLOR.roundedRectStroke(x, y, size, size, 4.2f * scale, 1.0f, 0.55f * scale, LayoutRender2D.alpha(palette.panelMuted(), 0.62f * headOpacity)); - } - - private void renderScrollbar(float x, float y, float w, float h, float maxScroll, float scale, SettingsGuiPalette palette, float opacity) { - updateScrollbarMetrics(x, y, w, h, maxScroll, scale); - if (!scrollbarVisible) return; - if (isScrollbarHovered(ClickGuiRenderer.getMouseX(), ClickGuiRenderer.getMouseY()) || draggingScrollbar) { - SystemCursor.set(SystemCursor.CursorType.SCROLL); - } - LayoutRender2D.roundedQuad(scrollbarX, scrollbarY, scrollbarW, scrollbarH, 2f * scale, - fade(palette.panelScrollTrackA(), opacity), fade(palette.panelScrollTrackB(), opacity), fade(palette.panelScrollTrackB(), opacity), fade(palette.panelScrollTrackA(), opacity)); - LayoutRender2D.roundedQuad(scrollbarX, scrollbarThumbY, scrollbarW, scrollbarThumbH, 2f * scale, - fade(palette.panelScrollHandleA(), opacity), fade(palette.panelScrollHandleB(), opacity), fade(palette.panelScrollHandleB(), opacity), fade(palette.panelScrollHandleA(), opacity)); - } - - private void updateScrollbarMetrics(float x, float y, float w, float h, float maxScroll, float scale) { - scrollbarVisible = maxScroll > 0.5f; - scrollbarMaxScroll = Math.max(0f, maxScroll); - if (!scrollbarVisible) { - draggingScrollbar = false; - scrollbarX = scrollbarY = scrollbarW = scrollbarH = scrollbarThumbY = scrollbarThumbH = 0f; - return; - } - scrollbarW = 3f * scale; - scrollbarX = x + w - scrollbarW - 2f * scale; - scrollbarY = y + 3f * scale; - scrollbarH = Math.max(1f, h - 6f * scale); - scrollbarThumbH = Math.max(18f * scale, scrollbarH * (scrollbarH / (scrollbarMaxScroll + scrollbarH))); - float scrollRatio = scrollbarMaxScroll <= 0f ? 0f : (-smoothedScroll / scrollbarMaxScroll); - scrollbarThumbY = scrollbarY + (scrollbarH - scrollbarThumbH) * AnimationUtility.clamp(scrollRatio, 0f, 1f); - } - - private boolean isScrollbarHovered(float mx, float my) { - if (!scrollbarVisible) return false; - float pad = 4f; - return ClickGuiMath.insideRect(mx, my, scrollbarX - pad, scrollbarY - pad, scrollbarW + pad * 2f, scrollbarH + pad * 2f); - } - - private void scrollToMouse(float my) { - if (!scrollbarVisible || scrollbarMaxScroll <= 0f) return; - float span = scrollbarH - scrollbarThumbH; - if (span <= 0.5f) return; - float thumbTop = AnimationUtility.clamp(my - scrollbarDragOffset, scrollbarY, scrollbarY + span); - float ratio = (thumbTop - scrollbarY) / span; - scroll = -scrollbarMaxScroll * AnimationUtility.clamp(ratio, 0f, 1f); - } - - private static int fade(int color, float opacity) { - return LayoutRender2D.alpha(color, AnimationUtility.clamp01(opacity)); - } - - private int modeColor() { - return mode.color(PlayerRelations.get()) | 0xFF000000; - } - - private static int pingBaseColor(int latency) { - if (latency < 0) return 0xFF8993A3; - if (latency < 90) return 0xFF52F2A1; - if (latency < 180) return 0xFFFFD166; - return 0xFFFF5A6E; - } - - private static int pingColor(int latency) { - if (latency < 0) return 0xFF9AA5B5; - if (latency < 75) return 0xFF66E086; - if (latency < 150) return 0xFFE8D36E; - if (latency < 300) return 0xFFFFA756; - return 0xFFFF5E6A; - } - - private static String pingText(int latency) { - return latency < 0 ? "?" : String.valueOf(latency); - } - - private static String modeDisplayName(DefineTarget.RelationTargetMode mode) { - if (mode == null) return ""; - return tr("mode_name." + mode.id(), mode.fallbackLabel()); - } - - private static String relationLabel(CategoryType type) { - if (type == null) return tr("relation.default", "Default"); - return switch (type) { - case FRIEND -> tr("relation.friend", "Friend"); - case ENEMY -> tr("relation.enemy", "Enemy"); - case STAFF -> tr("relation.staff", "Staff"); - case BEDWARS_SELF -> tr("relation.bedwars_self", "Bedwars Self"); - case BEDWARS_ENEMY -> tr("relation.bedwars_enemy", "Bedwars Enemy"); - default -> tr("relation.default", "Default"); - }; - } - - private static String teamName(PlayerInfo info) { - return info != null && info.getTeam() != null && info.getTeam().getName() != null ? info.getTeam().getName() : ""; - } - - private static String profileName(PlayerInfo info) { - GameProfile profile = info != null ? info.getProfile() : null; - return profile != null && profile.name() != null ? profile.name() : ""; - } - - private void setStatus(String text) { - if (statusSink != null) statusSink.accept(text); - } - - private static String tr(String key, String fallback, Object... args) { - return ClickGuiI18n.tr(I18N + key, fallback, args); - } - - private record PlayerRow(UUID id, - GameProfile profile, - String name, - Component displayName, - String searchText, - CategoryType relation, - int relationColor, - int latency, - boolean spectator) { - } - - private record RowHit(String name, Rect rect) { - } - - private record Rect(float x, float y, float w, float h) { - static final Rect ZERO = new Rect(0f, 0f, 0f, 0f); - - boolean contains(float mx, float my) { - return ClickGuiMath.insideRect(mx, my, x, y, w, h); - } - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/ClickGuiPickerState.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/ClickGuiPickerState.java deleted file mode 100644 index 3f1f1b7..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/ClickGuiPickerState.java +++ /dev/null @@ -1,970 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.features.gui.clickgui.picker; - -import dev.loki.lovisual.features.theme.Theme; -import dev.loki.lovisual.render.engine.text.font.FontFace; -import dev.loki.lovisual.render.engine.text.font.Fonts; -import dev.loki.lovisual.render.engine.text.font.faces.FontFamily; -import dev.loki.lovisual.render.engine.text.font.info.FontInfo; -import dev.loki.lovisual.render.engine.text.glyph.CustomTextRenderer; -import dev.loki.lovisual.render.engine.text.render.TextRenderer; -import net.minecraft.util.Util; -import net.minecraft.world.item.ItemStack; -import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; -import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; -import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.LayoutRender2D; -import dev.loki.lovisual.features.gui.clickgui.settings.owner.PickerDetailOwner; -import dev.loki.lovisual.features.gui.clickgui.settings.text.list.TextListSetting; -import dev.loki.lovisual.features.theme.Themes; -import dev.loki.lovisual.render.engine.animation.AnimationUtility; -import dev.loki.lovisual.render.helpers.clip.ScissorFunction; -import dev.loki.lovisual.render.helpers.util.SystemCursor; - -import java.util.*; -import dev.loki.lovisual.features.gui.clickgui.picker.catalog.PickerCatalog; -import dev.loki.lovisual.features.gui.clickgui.picker.catalog.PickerCatalogFactory; -import dev.loki.lovisual.features.gui.clickgui.picker.catalog.PickerEntryData; - -public final class ClickGuiPickerState { - private static final float SCALE = 2f; - - private static final float WINDOW_W = 340f * SCALE; - private static final float WINDOW_H = 260f * SCALE; - private static final float PADDING = 8f * SCALE; - private static final float HEADER_H = 28f * SCALE; - - private static final float SEARCH_H = 15f * SCALE; - private static final float FILTER_H = SEARCH_H; - private static final float SEARCH_W = 98f * SCALE; - private static final float FILTER_W = 80f * SCALE; - private static final float CLOSE_SIZE = 12f * SCALE; - - private static final float CARD_SIZE = 38f * SCALE; - private static final float CARD_GAP = 5f * SCALE; - private static final float SCROLL_STEP = 15f * SCALE; - private static final float MARQUEE_SPEED = 18f * SCALE; - private static final float MARQUEE_GAP = 10f * SCALE; - private static final float MARQUEE_PAUSE_SEC = 0.55f; - private static final Comparator ENTRY_ORDER = Comparator - .comparing(PickerEntryData::label, String.CASE_INSENSITIVE_ORDER) - .thenComparing(PickerEntryData::id, String.CASE_INSENSITIVE_ORDER); - private final TextListSetting owner; - private final TextListSetting.PickerMode mode; - private final PickerCatalog catalog; - private final String title; - private final StringBuilder search = new StringBuilder(); - private final List allEntries = new ArrayList<>(); - private final List visibleAll = new ArrayList<>(); - private final List visibleSelected = new ArrayList<>(); - private final Map byId = new LinkedHashMap<>(); - private final List cardHits = new ArrayList<>(); - private boolean showAll = true; - private boolean listening; - private float modeAnim; - private float scroll; - private float smoothScroll; - private boolean draggingScrollbar; - private boolean scrollbarVisible; - private float sbTrackX; - private float sbTrackY; - private float sbTrackW; - private float sbTrackH; - private float sbThumbH; - private float contentHeight; - private float windowX; - private float windowY; - private float searchX; - private float searchY; - private float filterX; - private float filterY; - private float closeX; - private float closeY; - private float gridX; - private float gridY; - private float gridW; - private float gridH; - private float detailX; - private float detailY; - private float detailW; - private float detailH; - private String focusedId; - private TextRenderer interBold; - private String hoveredCardId; - private long hoveredCardStartMs; - private float allFilterHoverAnim; - private float selectedFilterHoverAnim; - - public ClickGuiPickerState(TextListSetting owner, String title, TextListSetting.PickerMode mode) { - this.owner = owner; - this.mode = mode == null ? TextListSetting.PickerMode.TEXT : mode; - this.catalog = PickerCatalogFactory.forMode(this.mode); - this.title = title == null || title.isBlank() ? "Select" : title; - reloadEntries(); - refreshFiltered(true); - } - - private static boolean inside(float mx, float my, float x, float y, float w, float h) { - return mx >= x && mx <= x + w && my >= y && my <= y + h; - } - - private static float clamp(float v, float min, float max) { - if (v < min) return min; - return Math.min(v, max); - } - - public boolean isListening() { - return listening; - } - - public void toggleListening() { - listening = !listening; - } - - public void stopListening() { - listening = false; - } - - public void insertChar(char c) { - if (Character.isISOControl(c)) return; - search.append(c); - refreshFiltered(true); - } - - public void backspace() { - if (search.length() <= 0) return; - search.deleteCharAt(search.length() - 1); - refreshFiltered(true); - } - - public void handleMouseMove(float mx, float my) { - if (!draggingScrollbar) return; - layout(ClickGuiRenderer.framebufferWidth(), ClickGuiRenderer.framebufferHeight()); - scrollToPosition(my); - } - - public void handleMouseDown(float mx, float my, int button) { - if (button != 0) return; - - layout(ClickGuiRenderer.framebufferWidth(), ClickGuiRenderer.framebufferHeight()); - - if (!inside(mx, my, windowX, windowY, windowW(), WINDOW_H)) { - ClickGuiRenderer.closePickerScreen(); - return; - } - - if (inside(mx, my, closeX, closeY, CLOSE_SIZE, CLOSE_SIZE)) { - ClickGuiRenderer.closePickerScreen(); - return; - } - - if (inside(mx, my, searchX, searchY, SEARCH_W, SEARCH_H)) { - listening = true; - return; - } - listening = false; - - if (inside(mx, my, filterX, filterY, FILTER_W, FILTER_H)) { - float half = FILTER_W * 0.5f; - boolean nextShowAll = mx < filterX + half; - if (showAll != nextShowAll) { - showAll = nextShowAll; - scroll = 0f; - smoothScroll = 0f; - draggingScrollbar = false; - clampScroll(); - } - return; - } - - PickerDetailOwner detailOwner = detailOwner(); - if (detailOwner != null && inside(mx, my, detailX, detailY, detailW, detailH)) { - if (detailOwner.mouseClickedPickerDetails(mx, my, button)) return; - } - - if (handleScrollbarMouseDown(mx, my)) return; - - for (CardHit hit : cardHits) { - if (!inside(mx, my, hit.x(), hit.y(), hit.w(), hit.h())) continue; - focusedId = hit.getId(); - if (detailOwner != null) { - detailOwner.onPickerFocusChanged(hit.getId()); - if (!detailOwner.shouldToggleSelectionOnCardClick()) { - return; - } - } - toggleSelection(hit.getId()); - return; - } - } - - public void handleMouseUp(float mx, float my, int button) { - PickerDetailOwner detailOwner = detailOwner(); - if (detailOwner != null) { - detailOwner.mouseReleasedPickerDetails(mx, my, button); - } - if (button == 0) { - draggingScrollbar = false; - } - } - - public void scroll(double delta) { - layout(ClickGuiRenderer.framebufferWidth(), ClickGuiRenderer.framebufferHeight()); - - PickerDetailOwner detailOwner = detailOwner(); - if (detailOwner != null) { - float mx = ClickGuiRenderer.getMouseX(); - float my = ClickGuiRenderer.getMouseY(); - if (inside(mx, my, detailX, detailY, detailW, detailH) - && detailOwner.mouseScrolledPickerDetails(mx, my, delta)) { - return; - } - } - - scroll += (float) (delta * SCROLL_STEP); - clampScroll(); - } - - public void render(int fbw, int fbh) { - layout(fbw, fbh); - clampScroll(); - - float targetMode = showAll ? 0f : 1f; - modeAnim = AnimationUtility.approach(modeAnim, targetMode, 0.22f); - modeAnim = AnimationUtility.snap(modeAnim, targetMode, 0.01f); - smoothScroll = AnimationUtility.approach(smoothScroll, scroll, 0.2f); - smoothScroll = AnimationUtility.snap(smoothScroll, scroll, 0.05f); - - float mx = ClickGuiRenderer.getMouseX(); - float my = ClickGuiRenderer.getMouseY(); - - SettingsGuiPalette palette = SettingsGuiPalette.current(); - Themes.Theme theme = Theme.theme(); - - drawWindow(palette); - drawHeader(palette); - drawCloseButton(mx, my, palette, theme); - drawFilterPill(mx, my, palette, theme); - drawSearchField(mx, my, palette, theme); - drawGrid(mx, my, palette, theme); - drawPickerDetails(mx, my); - drawScrollbar(mx, my, palette); - } - - private void drawWindow(SettingsGuiPalette palette) { - ClickGuiRenderer.drawBlur(windowX, windowY, windowW(), WINDOW_H, 8f * SCALE, 0xFF000000, 200f / 255f); - LayoutRender2D.roundedSoftShadow( - windowX, - windowY, - windowW(), - WINDOW_H, - 8f * SCALE, - 8f * SCALE, - 0.018f, - palette.menuShadow() - ); - LayoutRender2D.roundedQuad( - windowX, - windowY, - windowW(), - WINDOW_H, - 8f * SCALE, - palette.menuWindowBgLeft(), - palette.menuWindowBgRight(), - palette.menuWindowBgRight(), - palette.menuWindowBgLeft() - ); - LayoutRender2D.roundedStroke( - windowX, - windowY, - windowW(), - WINDOW_H, - 8f * SCALE, - 0.1f * SCALE, - palette.menuWindowStroke() - ); - } - - private void drawHeader(SettingsGuiPalette palette) { - float sepY = windowY + HEADER_H; - LayoutRender2D.rectQuad( - windowX + PADDING, - sepY, - windowW() - PADDING * 2f, - 0.5f * SCALE, - palette.menuLineStrong(), - palette.menuLineMid(), - palette.menuLineMid(), - palette.menuLineStrong() - ); - - ClickGuiRenderer.drawText( - ClickGuiRenderer.getInterRegular(), - title, - windowX + PADDING, - windowY + 7f * SCALE, - 11f * SCALE, - palette.menuHeaderText(), - false - ); - - String count = owner.getValueSet().size() + " selected"; - ClickGuiRenderer.drawText( - ClickGuiRenderer.getInterRegular(), - count, - windowX + PADDING, - windowY + 15.5f * SCALE, - 8f * SCALE, - palette.panelMuted(), - false - ); - } - - private void drawCloseButton(float mx, float my, SettingsGuiPalette palette, Themes.Theme theme) { - boolean hover = inside(mx, my, closeX, closeY, CLOSE_SIZE, CLOSE_SIZE); - int base = hover ? palette.panelPillActive() : palette.panelPillBase(); - LayoutRender2D.rounded( - closeX, - closeY, - CLOSE_SIZE, - CLOSE_SIZE, - 3.5f * SCALE, - base - ); - LayoutRender2D.roundedStroke( - closeX, - closeY, - CLOSE_SIZE, - CLOSE_SIZE, - 3.5f * SCALE, - 0.5f * SCALE, - palette.panelStroke() - ); - - float pad = 3f * SCALE; - float x1 = closeX + pad; - float y1 = closeY + pad; - float x2 = closeX + CLOSE_SIZE - pad; - float y2 = closeY + CLOSE_SIZE - pad; - int c = hover ? theme.accent() : palette.panelText(); - ClickGuiRenderer.drawLine(x1, y1, x2, y2, c); - ClickGuiRenderer.drawLine(x2, y1, x1, y2, c); - } - - private void drawFilterPill(float mx, float my, SettingsGuiPalette palette, Themes.Theme theme) { - boolean hover = inside(mx, my, filterX, filterY, FILTER_W, FILTER_H); - boolean allHover = hover && mx < filterX + FILTER_W * 0.5f; - boolean selectedHover = hover && !allHover; - float dt = AnimationUtility.deltaTime(); - allFilterHoverAnim = AnimationUtility.snap( - AnimationUtility.approach(allFilterHoverAnim, allHover ? 1f : 0f, dt, 12f), - allHover ? 1f : 0f, - 0.01f - ); - selectedFilterHoverAnim = AnimationUtility.snap( - AnimationUtility.approach(selectedFilterHoverAnim, selectedHover ? 1f : 0f, dt, 12f), - selectedHover ? 1f : 0f, - 0.01f - ); - - int baseLeft = SettingsGuiPalette.mix(palette.panelPillBase(), palette.panelBgLeft(), 0.34f); - int baseRight = SettingsGuiPalette.mix(palette.panelPillBase(), palette.panelBgRight(), 0.46f); - if (hover) { - float hoverAnim = Math.max(allFilterHoverAnim, selectedFilterHoverAnim); - baseLeft = SettingsGuiPalette.mix(baseLeft, palette.menuCategoryHoverLeft(), 0.18f * hoverAnim); - baseRight = SettingsGuiPalette.mix(baseRight, palette.menuCategoryHoverRight(), 0.18f * hoverAnim); - } - - ClickGuiRenderer.drawBlur(filterX, filterY, FILTER_W, FILTER_H, 4f * SCALE, palette.panelBlurTint(), 150f / 255f); - LayoutRender2D.roundedQuad( - filterX, - filterY, - FILTER_W, - FILTER_H, - 3.5f * SCALE, - baseLeft, - baseRight, - baseRight, - baseLeft - ); - LayoutRender2D.roundedStrokeQuad( - filterX, - filterY, - FILTER_W, - FILTER_H, - 3.5f * SCALE, - 0.5f * SCALE, - SettingsGuiPalette.mix(palette.panelStroke(), palette.moduleDividerStart(), 0.20f), - SettingsGuiPalette.mix(palette.panelStroke(), palette.moduleDividerEnd(), 0.20f), - SettingsGuiPalette.mix(palette.panelStroke(), palette.moduleDividerEnd(), 0.20f), - SettingsGuiPalette.mix(palette.panelStroke(), palette.moduleDividerStart(), 0.20f) - ); - - float pad = 1.2f * SCALE; - float half = (FILTER_W - pad * 2f) * 0.5f; - float activeX = filterX + pad + half * modeAnim; - float activeHover = showAll ? allFilterHoverAnim : selectedFilterHoverAnim; - int activeA = SettingsGuiPalette.mix(palette.panelPillActive(), SettingsGuiPalette.withAlpha(theme.accentSoft(), 230), 0.18f + activeHover * 0.10f); - int activeB = SettingsGuiPalette.mix(palette.panelPillActive(), SettingsGuiPalette.withAlpha(theme.accent(), 235), 0.12f + activeHover * 0.14f); - int activeC = SettingsGuiPalette.mix(SettingsGuiPalette.darken(palette.panelPillActive(), 0.18f), SettingsGuiPalette.withAlpha(theme.accentSoft(), 215), 0.14f + activeHover * 0.10f); - LayoutRender2D.roundedQuad( - activeX, - filterY + pad, - half, - FILTER_H - pad * 2f, - 3f * SCALE, - activeA, - activeB, - activeC, - activeA - ); - LayoutRender2D.roundedStrokeQuad( - activeX, - filterY + pad, - half, - FILTER_H - pad * 2f, - 3f * SCALE, - 0.35f * SCALE, - SettingsGuiPalette.withAlpha(theme.accentSoft(), 155), - SettingsGuiPalette.withAlpha(theme.accent(), 165), - SettingsGuiPalette.withAlpha(theme.accentSoft(), 135), - SettingsGuiPalette.withAlpha(theme.accentSoft(), 155) - ); - - drawFilterDivider(filterX + pad + half, palette, theme); - - TextRenderer font = ClickGuiRenderer.getInterRegular(); - float textSize = 7.5f * SCALE; - float allW = ClickGuiRenderer.textWidth(font, "All", textSize); - float allH = ClickGuiRenderer.textHeight(font, textSize); - float selW = ClickGuiRenderer.textWidth(font, "Selected", textSize); - float selH = ClickGuiRenderer.textHeight(font, textSize); - - float allCx = filterX + half * 0.5f + pad; - float selCx = filterX + half + half * 0.5f + pad; - float cy = filterY + (FILTER_H - allH) * 0.5f; - - int allColor = showAll - ? palette.panelText() - : SettingsGuiPalette.mix(palette.panelMuted(), palette.panelText(), 0.22f * allFilterHoverAnim); - int selColor = !showAll - ? palette.panelText() - : SettingsGuiPalette.mix(palette.panelMuted(), palette.panelText(), 0.22f * selectedFilterHoverAnim); - ClickGuiRenderer.drawText(font, "All", allCx - allW * 0.5f, cy, textSize, allColor, false); - ClickGuiRenderer.drawText(font, "Selected", selCx - selW * 0.5f, filterY + (FILTER_H - selH) * 0.5f, textSize, selColor, false); - } - - private void drawFilterDivider(float x, SettingsGuiPalette palette, Themes.Theme theme) { - float backingW = Math.max(1.25f * SCALE, 1.05f); - float backingH = FILTER_H - 4.4f * SCALE; - float backingY = filterY + (FILTER_H - backingH) * 0.5f; - int backingTop = SettingsGuiPalette.withAlpha(palette.panelBgLeft(), 104); - int backingBottom = SettingsGuiPalette.withAlpha(palette.panelBgRight(), 84); - LayoutRender2D.roundedQuad( - x - backingW * 0.5f, - backingY, - backingW, - backingH, - backingW * 0.5f, - backingTop, - backingTop, - backingBottom, - backingBottom - ); - - float lineW = Math.max(0.5f * SCALE, 0.75f); - float lineH = FILTER_H - 6.0f * SCALE; - float lineY = filterY + (FILTER_H - lineH) * 0.5f; - int top = SettingsGuiPalette.withAlpha(SettingsGuiPalette.mix(palette.moduleDividerStart(), theme.accentSoft(), 0.30f), 154); - int mid = SettingsGuiPalette.withAlpha(SettingsGuiPalette.mix(palette.moduleDividerEnd(), theme.accent(), 0.34f), 214); - int bottom = SettingsGuiPalette.withAlpha(SettingsGuiPalette.mix(palette.moduleDividerEnd(), palette.panelText(), 0.22f), 142); - LayoutRender2D.roundedQuad( - x - lineW * 0.5f, - lineY, - lineW, - lineH, - lineW * 0.5f, - top, - mid, - bottom, - top - ); - - } - - private void drawSearchField(float mx, float my, SettingsGuiPalette palette, Themes.Theme theme) { - boolean hover = inside(mx, my, searchX, searchY, SEARCH_W, SEARCH_H); - boolean active = listening; - - int bgL = palette.panelBgLeft(); - int bgR = palette.panelBgRight(); - if (hover) { - bgL = ClickGuiRenderer.mixColor(bgL, palette.panelPillActive(), 0.18f); - bgR = ClickGuiRenderer.mixColor(bgR, palette.panelPillActive(), 0.18f); - } - if (active) { - bgL = ClickGuiRenderer.mixColor(bgL, theme.accentSoft(), 0.16f); - bgR = ClickGuiRenderer.mixColor(bgR, theme.accentSoft(), 0.16f); - } - - ClickGuiRenderer.drawBlur(searchX, searchY, SEARCH_W, SEARCH_H, 3f * SCALE, palette.panelBlurTint(), 170f / 255f); - LayoutRender2D.roundedQuad( - searchX, - searchY, - SEARCH_W, - SEARCH_H, - 3f * SCALE, - bgL, - bgR, - bgR, - bgL - ); - LayoutRender2D.roundedStroke( - searchX, - searchY, - SEARCH_W, - SEARCH_H, - 3f * SCALE, - 0.5f * SCALE, - palette.panelStroke() - ); - - float dividerX = searchX + SEARCH_W - 13f * SCALE; - LayoutRender2D.rect( - dividerX, - searchY + 3f * SCALE, - 0.5f * SCALE, - SEARCH_H - 6f * SCALE, - SettingsGuiPalette.withAlpha(palette.panelMuted(), 95) - ); - - TextRenderer font = ClickGuiRenderer.getInterRegular(); - float textSize = 7.5f * SCALE; - boolean empty = search.length() == 0; - boolean caret = active && AnimationUtility.blink(500L); - String shown = empty && !active ? "Search" : search.toString(); - if (active && caret) shown += "|"; - int color = empty && !active ? palette.panelMuted() : palette.panelText(); - float textY = searchY + (SEARCH_H - ClickGuiRenderer.textHeight(font, textSize)) * 0.5f; - float textX = searchX + 4f * SCALE; - boolean clipped = ScissorFunction.pushRaw(textX, searchY + SCALE, Math.max(1f, dividerX - textX - 1.5f * SCALE), SEARCH_H - 2f * SCALE); - ClickGuiRenderer.drawText(font, shown, textX, textY, textSize, color, false); - if (clipped) ScissorFunction.pop(); - - TextRenderer icons = Fonts.renderer("Icons", FontInfo.Type.Regular, ClickGuiRenderer.getInterRegular()); - String icon = "s"; - float iconSize = 8f * SCALE; - float iconW = ClickGuiRenderer.textWidth(icons, icon, iconSize); - float iconH = ClickGuiRenderer.textHeight(icons, iconSize); - float iconX = dividerX + ((searchX + SEARCH_W) - dividerX - iconW) * 0.5f; - float iconY = searchY + (SEARCH_H - iconH) * 0.5f - 0.35f * SCALE; - int iconColor = active ? palette.panelText() : palette.panelMuted(); - ClickGuiRenderer.drawText(icons, icon, iconX, iconY, iconSize, iconColor, false); - } - - private void drawGrid(float mx, float my, SettingsGuiPalette palette, Themes.Theme theme) { - List visible = visibleEntries(); - Set selected = owner.getValueSet(); - int columns = columns(); - - cardHits.clear(); - ClickGuiRenderer.beginPickerIconScissor(gridX, gridY, gridW, gridH); - boolean clipped = ScissorFunction.pushRaw(gridX, gridY, gridW, gridH); - - float step = CARD_SIZE + CARD_GAP; - float iconScale = 1.55f * SCALE; - float iconPx = 16f * iconScale; - TextRenderer font = ClickGuiRenderer.getInterRegular(); - float nameSize = 5.9f * SCALE; - boolean hasHoveredCard = false; - - for (int i = 0; i < visible.size(); i++) { - int col = i % columns; - int row = i / columns; - - float cardX = gridX + col * step; - float cardY = gridY + row * step + smoothScroll; - if (cardY + CARD_SIZE < gridY - 0.5f * SCALE) continue; - if (cardY > gridY + gridH + 0.5f * SCALE) continue; - - PickerEntryData entry = visible.get(i); - boolean isSelected = selected.contains(entry.getId()); - boolean hover = inside(mx, my, cardX, cardY, CARD_SIZE, CARD_SIZE); - if (hover) { - hasHoveredCard = true; - if (!entry.getId().equals(hoveredCardId)) { - hoveredCardId = entry.getId(); - hoveredCardStartMs = Util.getMillis(); - } - } - - int left = SettingsGuiPalette.withAlpha( - SettingsGuiPalette.mix(theme.windowBg(), theme.surface(), 0.42f), - 205 - ); - int right = SettingsGuiPalette.withAlpha( - SettingsGuiPalette.darken(SettingsGuiPalette.mix(theme.windowHeader(), theme.windowBg(), 0.55f), 0.26f), - 205 - ); - if (isSelected) { - left = ClickGuiRenderer.mixColor(left, theme.accentSoft(), 0.14f); - right = ClickGuiRenderer.mixColor(right, theme.accentSoft(), 0.26f); - } - if (hover) { - left = ClickGuiRenderer.mixColor(left, palette.moduleCardTopStrong(), 0.40f); - right = ClickGuiRenderer.mixColor(right, palette.moduleCardBottomStrong(), 0.42f); - } - - LayoutRender2D.roundedQuad( - cardX, - cardY, - CARD_SIZE, - CARD_SIZE, - 4f * SCALE, - left, - right, - right, - left - ); - - int stroke = isSelected ? theme.accentSoft() : palette.moduleDividerStart(); - float strokeW = isSelected ? 0.8f * SCALE : 0.45f * SCALE; - LayoutRender2D.roundedStroke(cardX, cardY, CARD_SIZE, CARD_SIZE, 4f * SCALE, strokeW, stroke); - - float lineY = cardY + CARD_SIZE - 11f * SCALE; - LayoutRender2D.rectQuad( - cardX + 3f * SCALE, - lineY, - CARD_SIZE - 6f * SCALE, - 0.45f * SCALE, - palette.moduleDividerStart(), - palette.moduleDividerEnd(), - palette.moduleDividerEnd(), - palette.moduleDividerStart() - ); - - ItemStack stack = entry.stack(); - if (stack != null && !stack.isEmpty()) { - float iconX = cardX + (CARD_SIZE - iconPx) * 0.5f; - float iconY = cardY + 4f * SCALE; - float clipX = cardX + SCALE; - float clipY = cardY + SCALE; - float clipW = CARD_SIZE - 2f * SCALE; - float clipH = Math.max(1f, lineY - clipY - 0.75f * SCALE); - ClickGuiRenderer.queuePickerIcon( - stack, - iconX, - iconY, - iconScale, - clipX, - clipY, - clipW, - clipH - ); - } else { - TextRenderer bold = interBold(); - float qSize = 13f * SCALE; - String q = "?"; - float qW = ClickGuiRenderer.textWidth(bold, q, qSize); - float qH = ClickGuiRenderer.textHeight(bold, qSize); - float iconAreaTop = cardY + 3.5f * SCALE; - float iconAreaBottom = lineY - SCALE; - float qX = cardX + (CARD_SIZE - qW) * 0.5f; - float qY = iconAreaTop + ((iconAreaBottom - iconAreaTop) - qH) * 0.5f; - int qColor = isSelected - ? SettingsGuiPalette.withAlpha(palette.moduleTitleText(), 210) - : SettingsGuiPalette.withAlpha(palette.panelMuted(), 190); - ClickGuiRenderer.drawText(bold, q, qX, qY, qSize, qColor, false); - } - - float textMaxW = CARD_SIZE - 6f * SCALE; - float textY = lineY + 1.5f * SCALE; - int textColor = isSelected ? palette.moduleTitleText() : palette.moduleDescriptionText(); - drawCardLabel(font, entry.label(), cardX, textY, textMaxW, nameSize, textColor, hover); - - cardHits.add(new CardHit(entry.getId(), cardX, cardY, CARD_SIZE, CARD_SIZE)); - } - if (!hasHoveredCard) { - hoveredCardId = null; - hoveredCardStartMs = 0L; - } - - if (clipped) { - ScissorFunction.pop(); - } - ClickGuiRenderer.endPickerIconScissor(); - - if (visible.isEmpty()) { - ClickGuiRenderer.drawText( - ClickGuiRenderer.getInterRegular(), - showAll ? "No entries" : "Nothing selected", - gridX + 4f * SCALE, - gridY + 4f * SCALE, - 7.2f * SCALE, - palette.panelMuted(), - false - ); - } - } - - private void drawScrollbar(float mx, float my, SettingsGuiPalette palette) { - if (!scrollbarVisible || contentHeight <= gridH + 0.5f) { - scrollbarVisible = false; - return; - } - - float baseW = 2.25f * SCALE; - boolean hover = isScrollbarHovered(mx, my) || draggingScrollbar; - if (hover) { - SystemCursor.set(SystemCursor.CursorType.SCROLL); - } - float trackW = hover ? 3.2f * SCALE : baseW; - float trackX = gridX + gridW - trackW - SCALE; - float trackY = gridY + SCALE; - float trackH = Math.max(12f * SCALE, gridH - 2f * SCALE); - - sbTrackX = trackX; - sbTrackY = trackY; - sbTrackW = trackW; - sbTrackH = trackH; - - LayoutRender2D.roundedQuad( - trackX, - trackY, - trackW, - trackH, - 2f * SCALE, - palette.moduleScrollTrackA(), - palette.moduleScrollTrackB(), - palette.moduleScrollTrackB(), - palette.moduleScrollTrackA() - ); - - float thumbH = Math.max(16f * SCALE, trackH * (gridH / contentHeight)); - sbThumbH = thumbH; - float ratio = contentHeight <= gridH ? 0f : (-smoothScroll / (contentHeight - gridH)); - float thumbY = trackY + (trackH - thumbH) * clamp(ratio, 0f, 1f); - - LayoutRender2D.roundedQuad( - trackX, - thumbY, - trackW, - thumbH, - 2f * SCALE, - palette.moduleScrollHandleA(), - palette.moduleScrollHandleB(), - palette.moduleScrollHandleB(), - palette.moduleScrollHandleA() - ); - } - - private void drawCardLabel(TextRenderer font, String label, float cardX, float textY, float maxW, float size, int color, boolean hover) { - String text = label == null ? "" : label; - float fullW = ClickGuiRenderer.textWidth(font, text, size); - if (!hover || fullW <= maxW + 0.5f * SCALE) { - String shown = ClickGuiRenderer.fitText(font, text, size, maxW); - float shownW = ClickGuiRenderer.textWidth(font, shown, size); - ClickGuiRenderer.drawText(font, shown, cardX + (CARD_SIZE - shownW) * 0.5f, textY, size, color, false); - return; - } - - float clipX = cardX + (CARD_SIZE - maxW) * 0.5f; - float textH = Math.max(1f, ClickGuiRenderer.textHeight(font, size)); - boolean clipped = ScissorFunction.pushRaw(clipX, textY - SCALE, maxW, textH + 2f * SCALE); - float elapsed = Math.max(0f, (Util.getMillis() - hoveredCardStartMs) / 1000f); - float cycleDistance = fullW + MARQUEE_GAP; - float cycleTime = MARQUEE_PAUSE_SEC + cycleDistance / Math.max(1f, MARQUEE_SPEED); - float t = elapsed % cycleTime; - float offset = t <= MARQUEE_PAUSE_SEC ? 0f : -(t - MARQUEE_PAUSE_SEC) * MARQUEE_SPEED; - - ClickGuiRenderer.drawText(font, text, clipX + offset, textY, size, color, false); - if (cycleDistance > maxW * 0.5f) { - ClickGuiRenderer.drawText(font, text, clipX + offset + cycleDistance, textY, size, color, false); - } - if (clipped) ScissorFunction.pop(); - } - - private float windowW() { - PickerDetailOwner detailOwner = detailOwner(); - return WINDOW_W + (detailOwner != null ? detailOwner.pickerDetailWidth() : 0f); - } - - private PickerDetailOwner detailOwner() { - return owner instanceof PickerDetailOwner detailOwner ? detailOwner : null; - } - - private void drawPickerDetails(float mx, float my) { - PickerDetailOwner detailOwner = detailOwner(); - if (detailOwner == null) return; - detailOwner.renderPickerDetails(detailX, detailY, detailW, detailH, focusedId, mx, my); - } - - private void layout(int fbw, int fbh) { - windowX = (fbw - windowW()) * 0.5f; - windowY = (fbh - WINDOW_H) * 0.5f; - - closeX = windowX + windowW() - PADDING - CLOSE_SIZE; - closeY = windowY + 7f * SCALE; - - searchX = closeX - 3f * SCALE - SEARCH_W; - searchY = windowY + 6f * SCALE; - - filterX = searchX - 3f * SCALE - FILTER_W; - filterY = searchY; - - gridX = windowX + PADDING; - gridY = windowY + HEADER_H + 6f * SCALE; - PickerDetailOwner detailOwner = detailOwner(); - detailW = detailOwner != null ? detailOwner.pickerDetailWidth() : 0f; - detailY = gridY; - detailH = WINDOW_H - (gridY - windowY) - PADDING; - detailX = windowX + windowW() - PADDING - detailW; - gridW = windowW() - PADDING * 2f - (detailOwner != null ? detailW + CARD_GAP : 0f); - gridH = detailH; - } - - private void reloadEntries() { - allEntries.clear(); - allEntries.addAll(catalog.entries(owner)); - } - - private void refreshFiltered(boolean resetScroll) { - String needle = search.toString().trim().toLowerCase(Locale.ROOT); - - byId.clear(); - for (PickerEntryData entry : allEntries) { - byId.put(entry.getId(), entry); - } - - visibleAll.clear(); - for (PickerEntryData entry : allEntries) { - if (!matches(entry, needle)) continue; - visibleAll.add(entry); - } - visibleAll.sort(ENTRY_ORDER); - - visibleSelected.clear(); - for (String id : owner.getValueSet()) { - PickerEntryData entry = byId.get(id); - if (entry == null) { - entry = new PickerEntryData(id, id, ItemStack.EMPTY); - } - if (!matches(entry, needle)) continue; - visibleSelected.add(entry); - } - visibleSelected.sort(ENTRY_ORDER); - - if (resetScroll) { - scroll = 0f; - smoothScroll = 0f; - draggingScrollbar = false; - } else { - clampScroll(); - } - } - - private List visibleEntries() { - return showAll ? visibleAll : visibleSelected; - } - - private void toggleSelection(String id) { - Set next = owner.getValueSet(); - if (next.contains(id)) { - next.remove(id); - } else { - next.add(owner.normalizeEntry(id)); - } - owner.setValueSet(next); - if (!next.contains(focusedId)) { - focusedId = next.isEmpty() ? null : next.iterator().next(); - PickerDetailOwner detailOwner = detailOwner(); - if (detailOwner != null && focusedId != null) detailOwner.onPickerFocusChanged(focusedId); - } - refreshFiltered(false); - } - - private boolean matches(PickerEntryData entry, String needle) { - if (needle == null || needle.isBlank()) return true; - String id = entry.getId() == null ? "" : entry.getId().toLowerCase(Locale.ROOT); - String label = entry.label() == null ? "" : entry.label().toLowerCase(Locale.ROOT); - return id.contains(needle) || label.contains(needle); - } - - private int columns() { - return Math.max(1, (int) Math.floor((gridW + CARD_GAP) / (CARD_SIZE + CARD_GAP))); - } - - private float computeContentHeight(int entriesCount) { - if (entriesCount <= 0) return 0f; - int cols = columns(); - int rows = (entriesCount + cols - 1) / cols; - return rows * CARD_SIZE + Math.max(0, rows - 1) * CARD_GAP; - } - - private void clampScroll() { - contentHeight = computeContentHeight(visibleEntries().size()); - float minScroll = Math.min(0f, gridH - contentHeight); - scroll = clamp(scroll, minScroll, 0f); - smoothScroll = clamp(smoothScroll, minScroll, 0f); - scrollbarVisible = contentHeight > gridH + 0.5f; - } - - private boolean handleScrollbarMouseDown(float mx, float my) { - if (!scrollbarVisible) return false; - if (!isScrollbarHovered(mx, my)) return false; - draggingScrollbar = true; - scrollToPosition(my); - return true; - } - - private boolean isScrollbarHovered(float mx, float my) { - if (!scrollbarVisible) return false; - float pad = 3f * SCALE; - return mx >= sbTrackX - pad && - mx <= sbTrackX + sbTrackW + pad && - my >= sbTrackY - pad && - my <= sbTrackY + sbTrackH + pad; - } - - private void scrollToPosition(float mouseY) { - if (contentHeight <= gridH + 0.5f) { - scroll = 0f; - smoothScroll = 0f; - return; - } - float trackSpan = sbTrackH - sbThumbH; - if (trackSpan <= 0f) return; - float rel = mouseY - sbTrackY - sbThumbH * 0.5f; - float t = clamp(rel / trackSpan, 0f, 1f); - float scrollRange = contentHeight - gridH; - scroll = -scrollRange * t; - smoothScroll = scroll; - } - - private TextRenderer interBold() { - if (interBold != null) return interBold; - FontFamily family = Fonts.getFamily("Inter"); - if (family == null) { - interBold = ClickGuiRenderer.getInterRegular(); - return interBold; - } - FontFace face = family.get(FontInfo.Type.Bold); - if (face == null) { - interBold = ClickGuiRenderer.getInterRegular(); - return interBold; - } - interBold = new CustomTextRenderer(face); - return interBold; - } - - private record CardHit(String id, float x, float y, float w, float h) { - public String getId() { - return id; - } - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/protocol/CombatProtocolHeuristicsEditorState.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/protocol/CombatProtocolHeuristicsEditorState.java deleted file mode 100644 index a2fee79..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/protocol/CombatProtocolHeuristicsEditorState.java +++ /dev/null @@ -1,932 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.features.gui.clickgui.protocol; - -import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; -import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; -import dev.loki.lovisual.features.gui.clickgui.settings.cooldown.ProtocolHeuristicsSetting; -import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiI18n; -import dev.loki.lovisual.features.theme.Theme; -import dev.loki.lovisual.features.theme.Themes; -import dev.loki.lovisual.render.engine.animation.AnimationUtility; -import dev.loki.lovisual.render.engine.text.render.TextRenderer; -import dev.loki.lovisual.render.helpers.clip.ScissorFunction; -import dev.loki.lovisual.render.helpers.util.SystemCursor; -import dev.loki.lovisual.util.combat.protocol.heuristics.CombatProtocolHeuristicSource; -import dev.loki.lovisual.util.combat.protocol.heuristics.CombatProtocolHeuristicsConfig; -import dev.loki.lovisual.util.text.edit.ClipboardUtil; -import dev.loki.lovisual.util.text.edit.TextSelection; -import org.lwjgl.glfw.GLFW; - -import java.util.ArrayList; -import java.util.EnumMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; -import java.util.regex.Pattern; -import java.util.regex.PatternSyntaxException; - -public final class CombatProtocolHeuristicsEditorState { - private static final float SCALE = 2f; - private static final float WINDOW_W = 420f * SCALE; - private static final float WINDOW_H = 280f * SCALE; - private static final float PADDING = 9f * SCALE; - private static final float HEADER_H = 31f * SCALE; - private static final float SOURCE_W = 108f * SCALE; - private static final float GAP = 7f * SCALE; - private static final float CLOSE_SIZE = 12f * SCALE; - - private final ProtocolHeuristicsSetting owner; - private final String title; - private final List sourceHits = new ArrayList<>(); - private final EnumMap sourceHover = - new EnumMap<>(CombatProtocolHeuristicSource.class); - private final EnumMap sourceSelect = - new EnumMap<>(CombatProtocolHeuristicSource.class); - private final EnumMap> editors = - new EnumMap<>(CombatProtocolHeuristicSource.class); - - private CombatProtocolHeuristicSource selected = CombatProtocolHeuristicSource.BOSSBAR; - private InlineEditor focusedEditor; - private float windowX; - private float windowY; - private float openAnim; - private Rect close = Rect.EMPTY; - private Rect reset = Rect.EMPTY; - private float closeHover; - private float resetHover; - - public CombatProtocolHeuristicsEditorState(ProtocolHeuristicsSetting owner, String title) { - this.owner = owner; - this.title = title == null || title.isBlank() ? tr("title", "Protocol heuristics") : title; - - CombatProtocolHeuristicsConfig config = CombatProtocolHeuristicsConfig.get(); - for (CombatProtocolHeuristicSource source : CombatProtocolHeuristicSource.values()) { - EnumMap byFamily = - new EnumMap<>(CombatProtocolHeuristicsConfig.ProtocolFamily.class); - for (CombatProtocolHeuristicsConfig.ProtocolFamily family : CombatProtocolHeuristicsConfig.ProtocolFamily.values()) { - byFamily.put(family, new InlineEditor(source, family, config.patterns(source, family))); - } - editors.put(source, byFamily); - sourceHover.put(source, 0f); - sourceSelect.put(source, source == selected ? 1f : 0f); - } - } - - public void handleMouseMove(float mx, float my) { - if (focusedEditor != null) focusedEditor.mouseMoved(mx, my); - } - - public void handleMouseDown(float mx, float my, int button) { - if (button != GLFW.GLFW_MOUSE_BUTTON_LEFT && button != GLFW.GLFW_MOUSE_BUTTON_RIGHT) return; - if (!inside(mx, my, windowX, windowY, WINDOW_W, WINDOW_H) || close.contains(mx, my)) { - ClickGuiRenderer.closeProtocolHeuristicsEditor(); - return; - } - - for (SourceHit hit : sourceHits) { - if (!hit.card().contains(mx, my)) continue; - blurEditor(); - selected = hit.source(); - boolean enabled = sourceEnabled(hit.source()); - if (button == GLFW.GLFW_MOUSE_BUTTON_LEFT) { - owner.setSourceEnabled(hit.source().key(), !enabled); - } - return; - } - - InlineEditor legacy = editor(selected, CombatProtocolHeuristicsConfig.ProtocolFamily.LEGACY); - InlineEditor modern = editor(selected, CombatProtocolHeuristicsConfig.ProtocolFamily.MODERN); - if (legacy.mousePressed(mx, my, button)) { - focus(legacy); - return; - } - if (modern.mousePressed(mx, my, button)) { - focus(modern); - return; - } - - if (button == GLFW.GLFW_MOUSE_BUTTON_LEFT && reset.contains(mx, my)) { - blurEditor(); - CombatProtocolHeuristicsConfig.get().reset(selected); - reloadEditors(selected); - return; - } - if (button == GLFW.GLFW_MOUSE_BUTTON_LEFT) blurEditor(); - } - - public void handleMouseUp(float mx, float my, int button) { - if (focusedEditor != null) focusedEditor.mouseReleased(button); - } - - public void scroll(double delta) { - float mx = ClickGuiRenderer.getMouseX(); - float my = ClickGuiRenderer.getMouseY(); - InlineEditor legacy = editor(selected, CombatProtocolHeuristicsConfig.ProtocolFamily.LEGACY); - InlineEditor modern = editor(selected, CombatProtocolHeuristicsConfig.ProtocolFamily.MODERN); - if (legacy.scroll(mx, my, delta)) return; - modern.scroll(mx, my, delta); - } - - public boolean charTyped(char chr) { - return focusedEditor != null && focusedEditor.charTyped(chr); - } - - public boolean keyPressed(int key, int scancode, int action, int modifiers) { - if (action != GLFW.GLFW_PRESS && action != GLFW.GLFW_REPEAT) return focusedEditor != null; - if (focusedEditor != null) { - if (key == GLFW.GLFW_KEY_ESCAPE) { - blurEditor(); - return true; - } - return focusedEditor.keyPressed(key, modifiers); - } - if (key == GLFW.GLFW_KEY_ESCAPE) { - ClickGuiRenderer.closeProtocolHeuristicsEditor(); - return true; - } - return true; - } - - public void render(int width, int height) { - float dt = AnimationUtility.deltaTime(); - openAnim = AnimationUtility.snap(AnimationUtility.approach(openAnim, 1f, dt, 8.5f), 1f, 0.008f); - float eased = easeOutCubic(openAnim); - windowX = (width - WINDOW_W) * 0.5f; - windowY = (height - WINDOW_H) * 0.5f + (1f - eased) * 10f * SCALE; - - SettingsGuiPalette palette = SettingsGuiPalette.current(); - Themes.Theme theme = Theme.theme(); - TextRenderer regular = ClickGuiRenderer.getInterRegular(); - TextRenderer medium = ClickGuiRenderer.getInterMedium(); - float mx = ClickGuiRenderer.getMouseX(); - float my = ClickGuiRenderer.getMouseY(); - - float previousAlpha = ClickGuiRenderer.setRenderAlphaMultiplier(eased); - try { - int shadow = SettingsGuiPalette.withAlpha(palette.menuShadow(), Math.round(255f * eased)); - ClickGuiRenderer.drawBlur(windowX, windowY, WINDOW_W, WINDOW_H, 9f * SCALE, 0xFF000000, 210f / 255f); - ClickGuiRenderer.drawRoundedRectShadow(windowX, windowY, WINDOW_W, WINDOW_H, 9f * SCALE, 13f * SCALE, 1.5f * SCALE, shadow); - ClickGuiRenderer.drawRoundedRectGradient( - windowX, windowY, WINDOW_W, WINDOW_H, 9f * SCALE, - palette.menuWindowBgLeft(), palette.menuWindowBgRight(), 0f - ); - ClickGuiRenderer.drawRoundedRectStrokeGradient( - windowX, windowY, WINDOW_W, WINDOW_H, 9f * SCALE, 0.55f * SCALE, - SettingsGuiPalette.mix(palette.menuWindowStroke(), theme.accentSoft(), 0.18f), - SettingsGuiPalette.mix(palette.menuWindowStroke(), theme.accent(), 0.10f), - 0f - ); - - renderHeader(regular, medium, mx, my, palette, theme); - - float contentY = windowY + HEADER_H + PADDING; - float contentH = WINDOW_H - HEADER_H - PADDING * 2f; - renderSources(windowX + PADDING, contentY, SOURCE_W, contentH, regular, medium, mx, my, palette, theme); - - float detailsX = windowX + PADDING + SOURCE_W + GAP; - float detailsW = WINDOW_W - PADDING * 2f - SOURCE_W - GAP; - renderDetails(detailsX, contentY, detailsW, contentH, regular, medium, mx, my, palette, theme); - } finally { - ClickGuiRenderer.restoreRenderAlphaMultiplier(previousAlpha); - } - } - - private void renderHeader(TextRenderer regular, TextRenderer medium, float mx, float my, - SettingsGuiPalette palette, Themes.Theme theme) { - float accentX = windowX + PADDING; - float accentY = windowY + 7f * SCALE; - ClickGuiRenderer.drawRoundedRect(accentX, accentY, 2f * SCALE, 17f * SCALE, SCALE, theme.accent()); - ClickGuiRenderer.drawRoundedRectGlow(accentX, accentY, 2f * SCALE, 17f * SCALE, SCALE, 7f * SCALE, - SettingsGuiPalette.withAlpha(theme.accent(), 90)); - - ClickGuiRenderer.drawText(medium, title, accentX + 7f * SCALE, windowY + 5.5f * SCALE, - 10.5f * SCALE, palette.menuHeaderText(), false); - String subtitle = tr("subtitle", "Configure protocol signals independently for each source"); - ClickGuiRenderer.drawText(regular, subtitle, accentX + 7f * SCALE, windowY + 17f * SCALE, - 6.3f * SCALE, palette.panelMuted(), false); - - float separatorY = windowY + HEADER_H; - ClickGuiRenderer.drawGradientRect(windowX + PADDING, separatorY, WINDOW_W - PADDING * 2f, - 0.5f * SCALE, palette.menuLineStrong(), palette.menuLineMid(), - palette.menuLineMid(), palette.menuLineStrong()); - - close = new Rect(windowX + WINDOW_W - PADDING - CLOSE_SIZE, windowY + 9f * SCALE, CLOSE_SIZE, CLOSE_SIZE); - closeHover = animate(closeHover, close.contains(mx, my), 14f); - int bg = SettingsGuiPalette.mix(palette.panelPillBase(), palette.panelPillActive(), 0.22f + closeHover * 0.58f); - ClickGuiRenderer.drawRoundedRect(close.x, close.y, close.w, close.h, 3.5f * SCALE, bg); - ClickGuiRenderer.drawRoundedRectStroke(close.x, close.y, close.w, close.h, 3.5f * SCALE, - 0.45f * SCALE, SettingsGuiPalette.mix(palette.panelStroke(), theme.accentSoft(), closeHover * 0.45f)); - float pad = 3.2f * SCALE; - int color = SettingsGuiPalette.mix(palette.panelMuted(), theme.accent(), closeHover); - ClickGuiRenderer.drawLine(close.x + pad, close.y + pad, close.x + close.w - pad, close.y + close.h - pad, color); - ClickGuiRenderer.drawLine(close.x + close.w - pad, close.y + pad, close.x + pad, close.y + close.h - pad, color); - if (close.contains(mx, my)) SystemCursor.set(SystemCursor.CursorType.HAND); - } - - private void renderSources(float x, float y, float w, float h, TextRenderer regular, TextRenderer medium, - float mx, float my, SettingsGuiPalette palette, Themes.Theme theme) { - sourceHits.clear(); - panel(x, y, w, h, palette); - ClickGuiRenderer.drawText(medium, tr("sources", "Sources"), x + 8f * SCALE, y + 7f * SCALE, - 7.2f * SCALE, palette.panelText(), false); - ClickGuiRenderer.drawText(regular, tr("source_controls", "LMB toggle · RMB configure"), - x + 8f * SCALE, y + 16f * SCALE, 5.3f * SCALE, palette.panelMuted(), false); - - float rowY = y + 28f * SCALE; - float rowH = 39f * SCALE; - float rowGap = 5f * SCALE; - for (CombatProtocolHeuristicSource source : CombatProtocolHeuristicSource.values()) { - Rect card = new Rect(x + 5f * SCALE, rowY, w - 10f * SCALE, rowH); - boolean hovered = card.contains(mx, my); - boolean active = source == selected; - boolean enabled = sourceEnabled(source); - float hover = animate(sourceHover.get(source), hovered, 13f); - float select = animate(sourceSelect.get(source), active, 11f); - sourceHover.put(source, hover); - sourceSelect.put(source, select); - - int baseLeft = SettingsGuiPalette.mix(palette.panelPillBase(), palette.menuCategoryHoverLeft(), hover * 0.30f); - int baseRight = SettingsGuiPalette.mix(palette.panelPillBase(), palette.menuCategoryHoverRight(), hover * 0.30f); - float accentMix = Math.max(enabled ? 0.42f : 0f, select * 0.32f); - int left = SettingsGuiPalette.mix(baseLeft, theme.accentSoft(), accentMix); - int right = SettingsGuiPalette.mix(baseRight, theme.accent(), accentMix * 0.72f); - ClickGuiRenderer.drawRoundedRectGradient(card.x, card.y, card.w, card.h, 5.5f * SCALE, left, right, 0f); - ClickGuiRenderer.drawRoundedRectStrokeGradient( - card.x, card.y, card.w, card.h, 5.5f * SCALE, 0.5f * SCALE, - SettingsGuiPalette.mix(palette.panelStroke(), theme.accentSoft(), Math.max(select, enabled ? 0.55f : 0f)), - SettingsGuiPalette.mix(palette.panelStroke(), theme.accent(), Math.max(select * 0.7f, enabled ? 0.32f : 0f)), - 0f - ); - - float markerW = (1.5f + 1.5f * select) * SCALE; - int marker = enabled ? theme.accent() : SettingsGuiPalette.withAlpha(palette.panelMuted(), 100); - ClickGuiRenderer.drawRoundedRect(card.x + 3f * SCALE, card.y + 7f * SCALE, markerW, - card.h - 14f * SCALE, markerW * 0.5f, marker); - if (enabled) { - ClickGuiRenderer.drawRoundedRectGlow(card.x + 3f * SCALE, card.y + 7f * SCALE, markerW, - card.h - 14f * SCALE, markerW * 0.5f, 5f * SCALE, - SettingsGuiPalette.withAlpha(theme.accent(), Math.round(55 + 55 * hover))); - } - - String name = sourceName(source); - ClickGuiRenderer.drawText(medium, name, card.x + 9f * SCALE, card.y + 7f * SCALE, - 7f * SCALE, palette.panelText(), false); - String state = enabled ? tr("enabled", "Enabled") : tr("disabled", "Disabled"); - int stateColor = enabled ? theme.accent() : palette.panelMuted(); - ClickGuiRenderer.drawText(regular, state, card.x + 9f * SCALE, card.y + 20f * SCALE, - 5.8f * SCALE, stateColor, false); - - int count = editor(source, CombatProtocolHeuristicsConfig.ProtocolFamily.LEGACY).patternCount() - + editor(source, CombatProtocolHeuristicsConfig.ProtocolFamily.MODERN).patternCount(); - String countLabel = tr("rules_short", "%s rules", count); - float countSize = 5.3f * SCALE; - float countW = ClickGuiRenderer.textWidth(regular, countLabel, countSize); - ClickGuiRenderer.drawText(regular, countLabel, card.x + card.w - 7f * SCALE - countW, - card.y + 20f * SCALE, countSize, palette.panelMuted(), false); - - sourceHits.add(new SourceHit(source, card)); - if (hovered) SystemCursor.set(SystemCursor.CursorType.HAND); - rowY += rowH + rowGap; - } - } - - private void renderDetails(float x, float y, float w, float h, TextRenderer regular, TextRenderer medium, - float mx, float my, SettingsGuiPalette palette, Themes.Theme theme) { - panel(x, y, w, h, palette); - boolean enabled = sourceEnabled(selected); - ClickGuiRenderer.drawText(medium, sourceName(selected), x + 8f * SCALE, y + 6f * SCALE, - 8.2f * SCALE, palette.panelText(), false); - String sourceHint = tr("source_hint." + selected.key(), defaultSourceHint(selected)); - ClickGuiRenderer.drawText(regular, sourceHint, x + 8f * SCALE, y + 17f * SCALE, - 5.8f * SCALE, palette.panelMuted(), false); - - String status = enabled ? tr("enabled", "Enabled") : tr("disabled", "Disabled"); - float statusSize = 5.7f * SCALE; - float statusW = ClickGuiRenderer.textWidth(medium, status, statusSize) + 12f * SCALE; - float statusX = x + w - 8f * SCALE - statusW; - int statusBg = enabled - ? SettingsGuiPalette.withAlpha(SettingsGuiPalette.mix(theme.accentSoft(), palette.panelPillActive(), 0.32f), 205) - : palette.panelPillBase(); - ClickGuiRenderer.drawRoundedRect(statusX, y + 7f * SCALE, statusW, 11f * SCALE, 5.5f * SCALE, statusBg); - ClickGuiRenderer.drawText(medium, status, statusX + 6f * SCALE, y + 9.6f * SCALE, - statusSize, enabled ? theme.accent() : palette.panelMuted(), false); - - float headingH = 29f * SCALE; - float footerH = 19f * SCALE; - float editorGap = 6f * SCALE; - float editorH = (h - headingH - footerH - editorGap * 2f) * 0.5f; - float editorY = y + headingH; - editor(selected, CombatProtocolHeuristicsConfig.ProtocolFamily.LEGACY) - .render(x + 6f * SCALE, editorY, w - 12f * SCALE, editorH, regular, medium, mx, my, palette, theme); - editorY += editorH + editorGap; - editor(selected, CombatProtocolHeuristicsConfig.ProtocolFamily.MODERN) - .render(x + 6f * SCALE, editorY, w - 12f * SCALE, editorH, regular, medium, mx, my, palette, theme); - - String resetLabel = tr("reset_source", "Reset source"); - float resetW = Math.max(58f * SCALE, ClickGuiRenderer.textWidth(regular, resetLabel, 5.8f * SCALE) + 14f * SCALE); - reset = new Rect(x + w - 6f * SCALE - resetW, y + h - 15f * SCALE, resetW, 11f * SCALE); - resetHover = animate(resetHover, reset.contains(mx, my), 13f); - button(reset, resetLabel, resetHover, regular, palette, theme); - if (reset.contains(mx, my)) SystemCursor.set(SystemCursor.CursorType.HAND); - - String hint = focusedEditor == null - ? tr("edit_hint", "Click a pattern area to edit") - : tr("keyboard_hint", "Ctrl+A/C/X/V · Enter new line · Esc unfocus"); - ClickGuiRenderer.drawText(regular, hint, x + 8f * SCALE, y + h - 12.3f * SCALE, - 5.2f * SCALE, palette.panelMuted(), false); - } - - private void focus(InlineEditor editor) { - if (focusedEditor == editor) return; - if (focusedEditor != null) focusedEditor.setFocused(false); - focusedEditor = editor; - if (focusedEditor != null) focusedEditor.setFocused(true); - } - - private void blurEditor() { - if (focusedEditor != null) focusedEditor.setFocused(false); - focusedEditor = null; - } - - private void reloadEditors(CombatProtocolHeuristicSource source) { - CombatProtocolHeuristicsConfig config = CombatProtocolHeuristicsConfig.get(); - for (CombatProtocolHeuristicsConfig.ProtocolFamily family : CombatProtocolHeuristicsConfig.ProtocolFamily.values()) { - editor(source, family).replace(config.patterns(source, family)); - } - } - - private InlineEditor editor(CombatProtocolHeuristicSource source, - CombatProtocolHeuristicsConfig.ProtocolFamily family) { - return editors.get(source).get(family); - } - - private boolean sourceEnabled(CombatProtocolHeuristicSource source) { - return owner.sources() != null && owner.sources().get(source.key()); - } - - private final class InlineEditor { - private static final float LINE_H = 8f * SCALE; - private static final float FONT_SIZE = 5.9f * SCALE; - private static final float GUTTER_W = 19f * SCALE; - private static final float TEXT_PAD = 5f * SCALE; - - private final CombatProtocolHeuristicSource source; - private final CombatProtocolHeuristicsConfig.ProtocolFamily family; - private final StringBuilder buffer = new StringBuilder(); - private final TextSelection selection = new TextSelection(); - private Rect bounds = Rect.EMPTY; - private Rect textArea = Rect.EMPTY; - private int caret; - private int preferredColumn = -1; - private float scrollY; - private float targetScrollY; - private float scrollX; - private float targetScrollX; - private float focusAnim; - private float hoverAnim; - private float blink; - private boolean focused; - private boolean dragging; - - InlineEditor(CombatProtocolHeuristicSource source, - CombatProtocolHeuristicsConfig.ProtocolFamily family, - Set patterns) { - this.source = source; - this.family = family; - replace(patterns); - } - - void replace(Set patterns) { - buffer.setLength(0); - if (patterns != null && !patterns.isEmpty()) buffer.append(String.join("\n", patterns)); - caret = Math.min(caret, buffer.length()); - selection.clear(); - targetScrollX = targetScrollY = scrollX = scrollY = 0f; - } - - void setFocused(boolean value) { - focused = value; - blink = 0f; - if (!value) { - dragging = false; - selection.clear(); - } - } - - int patternCount() { - return parsedPatterns().size(); - } - - void render(float x, float y, float w, float h, TextRenderer regular, TextRenderer medium, - float mx, float my, SettingsGuiPalette palette, Themes.Theme theme) { - bounds = new Rect(x, y, w, h); - boolean hovered = bounds.contains(mx, my); - focusAnim = animate(focusAnim, focused, 13f); - hoverAnim = animate(hoverAnim, hovered, 13f); - blink += AnimationUtility.deltaTime(); - scrollY = AnimationUtility.approach(scrollY, targetScrollY, 0.28f); - scrollX = AnimationUtility.approach(scrollX, targetScrollX, 0.28f); - - int baseA = SettingsGuiPalette.mix(palette.moduleCardTop(), palette.panelBgLeft(), 0.30f); - int baseB = SettingsGuiPalette.mix(palette.moduleCardBottom(), palette.panelBgRight(), 0.28f); - int bgA = SettingsGuiPalette.mix(baseA, theme.accentSoft(), focusAnim * 0.14f + hoverAnim * 0.04f); - int bgB = SettingsGuiPalette.mix(baseB, theme.accent(), focusAnim * 0.08f + hoverAnim * 0.03f); - ClickGuiRenderer.drawRoundedRectGradient(x, y, w, h, 5.5f * SCALE, bgA, bgB, 85f); - ClickGuiRenderer.drawRoundedRectStrokeGradient( - x, y, w, h, 5.5f * SCALE, (0.45f + focusAnim * 0.25f) * SCALE, - SettingsGuiPalette.mix(palette.panelStroke(), theme.accentSoft(), 0.18f + focusAnim * 0.66f), - SettingsGuiPalette.mix(palette.panelStroke(), theme.accent(), 0.10f + focusAnim * 0.54f), - 0f - ); - if (focused) { - ClickGuiRenderer.drawRoundedRectGlow(x, y, w, h, 5.5f * SCALE, 6f * SCALE, - SettingsGuiPalette.withAlpha(theme.accentSoft(), Math.round(25f + 25f * focusAnim))); - } - - boolean legacy = family == CombatProtocolHeuristicsConfig.ProtocolFamily.LEGACY; - String name = legacy ? tr("legacy", "Legacy (1.8)") : tr("modern", "Modern (1.9+)"); - int invalid = invalidCount(List.of(lines())); - String meta = tr("pattern_count", "%s patterns", patternCount()); - if (invalid > 0) meta += " · " + tr("invalid_count", "%s invalid", invalid); - ClickGuiRenderer.drawText(medium, name, x + 7f * SCALE, y + 5.5f * SCALE, - 6.7f * SCALE, palette.moduleTitleText(), false); - float metaSize = 5.3f * SCALE; - float metaW = ClickGuiRenderer.textWidth(regular, meta, metaSize); - ClickGuiRenderer.drawText(regular, meta, x + w - 7f * SCALE - metaW, y + 7f * SCALE, - metaSize, invalid > 0 ? 0xFFFF8A80 : palette.panelMuted(), false); - - float areaY = y + 19f * SCALE; - textArea = new Rect(x + 5f * SCALE, areaY, w - 10f * SCALE, h - 24f * SCALE); - int areaBg = SettingsGuiPalette.withAlpha(SettingsGuiPalette.darken(palette.panelBgRight(), 0.18f), 190); - ClickGuiRenderer.drawRoundedRect(textArea.x, textArea.y, textArea.w, textArea.h, 3.5f * SCALE, areaBg); - ClickGuiRenderer.drawRoundedRectStroke(textArea.x, textArea.y, textArea.w, textArea.h, 3.5f * SCALE, - 0.35f * SCALE, SettingsGuiPalette.mix(palette.panelStroke(), theme.accentSoft(), focusAnim * 0.42f)); - renderText(regular, palette, theme); - if (hovered) SystemCursor.set(SystemCursor.CursorType.TEXT); - } - - private void renderText(TextRenderer font, SettingsGuiPalette palette, Themes.Theme theme) { - String[] lines = lines(); - float viewX = textArea.x + GUTTER_W + TEXT_PAD - scrollX; - float viewY = textArea.y + 3f * SCALE - scrollY; - boolean clipped = ScissorFunction.pushRaw(textArea.x, textArea.y, textArea.w, textArea.h); - int globalOffset = 0; - int caretLine = 0; - int caretCol = 0; - - for (int i = 0; i < lines.length; i++) { - String line = lines[i]; - float lineY = viewY + i * LINE_H; - int lineStart = globalOffset; - int lineEnd = lineStart + line.length(); - if (caret >= lineStart && caret <= lineEnd) { - caretLine = i; - caretCol = caret - lineStart; - } - - if (lineY + LINE_H >= textArea.y && lineY <= textArea.y + textArea.h) { - if (focused && selection.hasRange() && selection.appliesToLine(0)) { - int selStart = Math.min(selection.anchor(), selection.caret()); - int selEnd = Math.max(selection.anchor(), selection.caret()); - int a = Math.max(lineStart, selStart); - int b = Math.min(lineEnd, selEnd); - if (b > a) { - float x0 = viewX + ClickGuiRenderer.textWidth(font, line.substring(0, a - lineStart), FONT_SIZE); - float x1 = viewX + ClickGuiRenderer.textWidth(font, line.substring(0, b - lineStart), FONT_SIZE); - ClickGuiRenderer.drawRoundedRect(x0, lineY, Math.max(1f, x1 - x0), LINE_H, - 2f * SCALE, SettingsGuiPalette.withAlpha(theme.accentSoft(), 155)); - } - } - - String lineNumber = Integer.toString(i + 1); - float numberW = ClickGuiRenderer.textWidth(font, lineNumber, 5.1f * SCALE); - ClickGuiRenderer.drawText(font, lineNumber, textArea.x + GUTTER_W - 4f * SCALE - numberW, - lineY + 0.5f * SCALE, 5.1f * SCALE, palette.panelMuted(), false); - int textColor = line.isBlank() || validPattern(line) ? palette.moduleDescriptionText() : 0xFFFF8A80; - ClickGuiRenderer.drawText(font, line, viewX, lineY + 0.2f * SCALE, FONT_SIZE, textColor, false); - } - globalOffset = lineEnd + 1; - } - - ClickGuiRenderer.drawRect(textArea.x + GUTTER_W, textArea.y + 2f * SCALE, - 0.4f * SCALE, textArea.h - 4f * SCALE, palette.panelDivider()); - - if (focused && ((int) (blink * 2f) & 1) == 0) { - String line = lines[Math.max(0, Math.min(caretLine, lines.length - 1))]; - float caretX = viewX + ClickGuiRenderer.textWidth(font, line.substring(0, Math.min(caretCol, line.length())), FONT_SIZE); - float caretY = viewY + caretLine * LINE_H; - ClickGuiRenderer.drawRoundedRect(caretX, caretY, 0.65f * SCALE, LINE_H, - 0.3f * SCALE, theme.accent()); - } - if (clipped) ScissorFunction.pop(); - } - - boolean mousePressed(float mx, float my, int button) { - if (button != GLFW.GLFW_MOUSE_BUTTON_LEFT || !textArea.contains(mx, my)) return false; - int target = caretFromPoint(mx, my); - boolean shift = shiftDown(); - if (shift) { - if (!selection.hasCaret()) selection.begin(0, caret); - selection.updateCaret(0, target); - } else { - selection.clear(); - selection.begin(0, target); - } - caret = target; - preferredColumn = -1; - dragging = true; - blink = 0f; - ensureCaretVisible(); - return true; - } - - void mouseMoved(float mx, float my) { - if (!focused || !dragging) return; - int target = caretFromPoint(mx, my); - if (!selection.hasCaret()) selection.begin(0, caret); - selection.updateCaret(0, target); - caret = target; - preferredColumn = -1; - blink = 0f; - ensureCaretVisible(); - } - - void mouseReleased(int button) { - if (button == GLFW.GLFW_MOUSE_BUTTON_LEFT) dragging = false; - } - - boolean scroll(float mx, float my, double delta) { - if (!bounds.contains(mx, my)) return false; - targetScrollY -= (float) delta * LINE_H * 2f; - clampScroll(); - return true; - } - - boolean charTyped(char chr) { - if (!focused || Character.isISOControl(chr)) return false; - insertText(String.valueOf(chr)); - return true; - } - - boolean keyPressed(int key, int modifiers) { - if (!focused) return false; - boolean ctrl = (modifiers & GLFW.GLFW_MOD_CONTROL) != 0; - boolean shift = (modifiers & GLFW.GLFW_MOD_SHIFT) != 0; - if (ctrl) { - switch (key) { - case GLFW.GLFW_KEY_A -> selectAll(); - case GLFW.GLFW_KEY_C -> copy(); - case GLFW.GLFW_KEY_X -> cut(); - case GLFW.GLFW_KEY_V -> insertText(ClipboardUtil.get()); - default -> { - return true; - } - } - return true; - } - switch (key) { - case GLFW.GLFW_KEY_ENTER, GLFW.GLFW_KEY_KP_ENTER -> insertText("\n"); - case GLFW.GLFW_KEY_BACKSPACE -> backspace(); - case GLFW.GLFW_KEY_DELETE -> deleteForward(); - case GLFW.GLFW_KEY_LEFT -> moveHorizontal(-1, shift); - case GLFW.GLFW_KEY_RIGHT -> moveHorizontal(1, shift); - case GLFW.GLFW_KEY_UP -> moveVertical(-1, shift); - case GLFW.GLFW_KEY_DOWN -> moveVertical(1, shift); - case GLFW.GLFW_KEY_HOME -> moveLineBoundary(false, shift); - case GLFW.GLFW_KEY_END -> moveLineBoundary(true, shift); - default -> { - return true; - } - } - return true; - } - - private void insertText(String text) { - if (text == null || text.isEmpty()) return; - deleteSelection(); - String normalized = text.replace("\r\n", "\n").replace('\r', '\n'); - buffer.insert(caret, normalized); - caret += normalized.length(); - selection.clear(); - preferredColumn = -1; - blink = 0f; - changed(); - } - - private void backspace() { - if (deleteSelection()) { - changed(); - return; - } - if (caret <= 0) return; - buffer.deleteCharAt(caret - 1); - caret--; - selection.clear(); - preferredColumn = -1; - changed(); - } - - private void deleteForward() { - if (deleteSelection()) { - changed(); - return; - } - if (caret >= buffer.length()) return; - buffer.deleteCharAt(caret); - selection.clear(); - preferredColumn = -1; - changed(); - } - - private boolean deleteSelection() { - if (!selection.hasRange()) return false; - int start = clamp(Math.min(selection.anchor(), selection.caret()), 0, buffer.length()); - int end = clamp(Math.max(selection.anchor(), selection.caret()), 0, buffer.length()); - if (end <= start) return false; - buffer.delete(start, end); - caret = start; - selection.clear(); - return true; - } - - private void moveHorizontal(int delta, boolean extend) { - int target; - if (!extend && selection.hasRange()) { - target = delta < 0 - ? Math.min(selection.anchor(), selection.caret()) - : Math.max(selection.anchor(), selection.caret()); - } else { - target = clamp(caret + delta, 0, buffer.length()); - } - updateSelection(target, extend); - caret = target; - preferredColumn = -1; - blink = 0f; - ensureCaretVisible(); - } - - private void moveVertical(int delta, boolean extend) { - String[] lines = lines(); - LineColumn current = lineColumn(caret, lines); - if (preferredColumn < 0) preferredColumn = current.column; - int targetLine = clamp(current.line + delta, 0, lines.length - 1); - int target = indexForLineColumn(lines, targetLine, Math.min(preferredColumn, lines[targetLine].length())); - updateSelection(target, extend); - caret = target; - blink = 0f; - ensureCaretVisible(); - } - - private void moveLineBoundary(boolean end, boolean extend) { - String[] lines = lines(); - LineColumn current = lineColumn(caret, lines); - int target = indexForLineColumn(lines, current.line, end ? lines[current.line].length() : 0); - updateSelection(target, extend); - caret = target; - preferredColumn = -1; - blink = 0f; - ensureCaretVisible(); - } - - private void updateSelection(int target, boolean extend) { - if (extend) { - if (!selection.hasCaret()) selection.begin(0, caret); - selection.updateCaret(0, target); - } else { - selection.clear(); - } - } - - private void selectAll() { - selection.begin(0, 0); - selection.updateCaret(0, buffer.length()); - caret = buffer.length(); - preferredColumn = -1; - blink = 0f; - ensureCaretVisible(); - } - - private void copy() { - if (selection.hasRange()) { - int start = clamp(Math.min(selection.anchor(), selection.caret()), 0, buffer.length()); - int end = clamp(Math.max(selection.anchor(), selection.caret()), 0, buffer.length()); - if (end > start) ClipboardUtil.copy(buffer.substring(start, end)); - } else if (!buffer.isEmpty()) { - ClipboardUtil.copy(buffer.toString()); - } - } - - private void cut() { - if (!selection.hasRange()) return; - copy(); - if (deleteSelection()) changed(); - } - - private void changed() { - CombatProtocolHeuristicsConfig.get().setPatterns(source, family, parsedPatterns()); - clampScroll(); - ensureCaretVisible(); - } - - private Set parsedPatterns() { - LinkedHashSet out = new LinkedHashSet<>(); - for (String line : lines()) { - String clean = line.trim(); - if (!clean.isEmpty()) out.add(clean); - } - return out; - } - - private int caretFromPoint(float mx, float my) { - String[] lines = lines(); - int line = clamp((int) Math.floor((my - textArea.y - 3f * SCALE + scrollY) / LINE_H), 0, lines.length - 1); - String text = lines[line]; - float localX = mx - (textArea.x + GUTTER_W + TEXT_PAD) + scrollX; - int column = 0; - TextRenderer font = ClickGuiRenderer.getInterRegular(); - for (int i = 1; i <= text.length(); i++) { - float prev = ClickGuiRenderer.textWidth(font, text.substring(0, i - 1), FONT_SIZE); - float next = ClickGuiRenderer.textWidth(font, text.substring(0, i), FONT_SIZE); - if (localX < (prev + next) * 0.5f) break; - column = i; - } - return indexForLineColumn(lines, line, column); - } - - private void ensureCaretVisible() { - String[] lines = lines(); - LineColumn pos = lineColumn(caret, lines); - float caretY = pos.line * LINE_H; - float visibleH = Math.max(LINE_H, textArea.h - 6f * SCALE); - if (caretY < targetScrollY) targetScrollY = caretY; - if (caretY + LINE_H > targetScrollY + visibleH) targetScrollY = caretY + LINE_H - visibleH; - - TextRenderer font = ClickGuiRenderer.getInterRegular(); - String line = lines[pos.line]; - float caretX = ClickGuiRenderer.textWidth(font, line.substring(0, Math.min(pos.column, line.length())), FONT_SIZE); - float visibleW = Math.max(20f * SCALE, textArea.w - GUTTER_W - TEXT_PAD * 2f); - if (caretX < targetScrollX) targetScrollX = caretX; - if (caretX + 2f * SCALE > targetScrollX + visibleW) targetScrollX = caretX + 2f * SCALE - visibleW; - clampScroll(); - } - - private void clampScroll() { - String[] lines = lines(); - float maxY = Math.max(0f, lines.length * LINE_H - Math.max(LINE_H, textArea.h - 6f * SCALE)); - float maxLineW = 0f; - TextRenderer font = ClickGuiRenderer.getInterRegular(); - for (String line : lines) maxLineW = Math.max(maxLineW, ClickGuiRenderer.textWidth(font, line, FONT_SIZE)); - float maxX = Math.max(0f, maxLineW - Math.max(20f * SCALE, textArea.w - GUTTER_W - TEXT_PAD * 2f)); - targetScrollY = clamp(targetScrollY, 0f, maxY); - targetScrollX = clamp(targetScrollX, 0f, maxX); - } - - private String[] lines() { - return buffer.toString().split("\n", -1); - } - } - - private static void panel(float x, float y, float w, float h, SettingsGuiPalette palette) { - ClickGuiRenderer.drawBlur(x, y, w, h, 5f * SCALE, palette.panelBlurTint(), 175f / 255f); - ClickGuiRenderer.drawRoundedRectGradient(x, y, w, h, 6f * SCALE, - palette.panelBgLeft(), palette.panelBgRight(), 0f); - ClickGuiRenderer.drawRoundedRectStroke(x, y, w, h, 6f * SCALE, - 0.5f * SCALE, palette.panelStroke()); - } - - private static void button(Rect rect, String label, float hover, TextRenderer font, - SettingsGuiPalette palette, Themes.Theme theme) { - int bg = SettingsGuiPalette.mix(palette.panelPillBase(), theme.accentSoft(), 0.08f + hover * 0.24f); - ClickGuiRenderer.drawRoundedRect(rect.x, rect.y, rect.w, rect.h, 3.5f * SCALE, bg); - ClickGuiRenderer.drawRoundedRectStroke(rect.x, rect.y, rect.w, rect.h, 3.5f * SCALE, - 0.4f * SCALE, SettingsGuiPalette.mix(palette.panelStroke(), theme.accent(), hover * 0.42f)); - float size = 5.8f * SCALE; - float tw = ClickGuiRenderer.textWidth(font, label, size); - float th = ClickGuiRenderer.textHeight(font, size); - ClickGuiRenderer.drawText(font, label, rect.x + (rect.w - tw) * 0.5f, - rect.y + (rect.h - th) * 0.5f, size, - SettingsGuiPalette.mix(palette.panelText(), theme.accent(), hover * 0.55f), false); - } - - private static float animate(float value, boolean active, float speed) { - float target = active ? 1f : 0f; - return AnimationUtility.snap( - AnimationUtility.approach(value, target, AnimationUtility.deltaTime(), speed), - target, - 0.01f - ); - } - - private static float easeOutCubic(float value) { - float t = clamp(value, 0f, 1f); - float inv = 1f - t; - return 1f - inv * inv * inv; - } - - private static int invalidCount(Iterable patterns) { - int invalid = 0; - if (patterns == null) return invalid; - for (String pattern : patterns) { - if (pattern != null && !pattern.isBlank() && !validPattern(pattern)) invalid++; - } - return invalid; - } - - private static boolean validPattern(String pattern) { - if (pattern == null || pattern.isBlank()) return false; - try { - Pattern.compile(pattern, Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE); - return true; - } catch (PatternSyntaxException ignored) { - return false; - } - } - - private static String sourceName(CombatProtocolHeuristicSource source) { - return ClickGuiI18n.tr( - "setting.common.combat.protocol_heuristics.option." + source.key(), - switch (source) { - case MESSAGES -> "Messages"; - case OVERLAY -> "Overlay"; - case BOSSBAR -> "Boss bar"; - } - ); - } - - private static String defaultSourceHint(CombatProtocolHeuristicSource source) { - return switch (source) { - case MESSAGES -> "Chat and system messages received from the server"; - case OVERLAY -> "Action bar and overlay text"; - case BOSSBAR -> "Visible boss bar titles"; - }; - } - - private static String tr(String suffix, String fallback, Object... args) { - return ClickGuiI18n.tr("clickgui.protocol_heuristics." + suffix, fallback, args); - } - - private static boolean shiftDown() { - long window = net.minecraft.client.Minecraft.getInstance().getWindow().handle(); - return GLFW.glfwGetKey(window, GLFW.GLFW_KEY_LEFT_SHIFT) == GLFW.GLFW_PRESS - || GLFW.glfwGetKey(window, GLFW.GLFW_KEY_RIGHT_SHIFT) == GLFW.GLFW_PRESS; - } - - private static LineColumn lineColumn(int index, String[] lines) { - int remaining = clamp(index, 0, totalLength(lines)); - for (int line = 0; line < lines.length; line++) { - int length = lines[line].length(); - if (remaining <= length) return new LineColumn(line, remaining); - remaining -= length + 1; - } - int last = Math.max(0, lines.length - 1); - return new LineColumn(last, lines[last].length()); - } - - private static int indexForLineColumn(String[] lines, int line, int column) { - int index = 0; - int resolvedLine = clamp(line, 0, lines.length - 1); - for (int i = 0; i < resolvedLine; i++) index += lines[i].length() + 1; - return index + clamp(column, 0, lines[resolvedLine].length()); - } - - private static int totalLength(String[] lines) { - int length = 0; - for (int i = 0; i < lines.length; i++) { - length += lines[i].length(); - if (i < lines.length - 1) length++; - } - return length; - } - - private static boolean inside(float mx, float my, float x, float y, float w, float h) { - return mx >= x && mx <= x + w && my >= y && my <= y + h; - } - - private static int clamp(int value, int min, int max) { - return Math.max(min, Math.min(max, value)); - } - - private static float clamp(float value, float min, float max) { - return Math.max(min, Math.min(max, value)); - } - - private record SourceHit(CombatProtocolHeuristicSource source, Rect card) { - } - - private record LineColumn(int line, int column) { - } - - private record Rect(float x, float y, float w, float h) { - static final Rect EMPTY = new Rect(0f, 0f, 0f, 0f); - - boolean contains(float mx, float my) { - return inside(mx, my, x, y, w, h); - } - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/Inventory.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/Inventory.java deleted file mode 100644 index b0d9e2e..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/Inventory.java +++ /dev/null @@ -1,476 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.features.gui.hud.draggable.impl.combat; - -import dev.loki.lovisual.config.values.color.KeyBindValue; -import dev.loki.lovisual.config.values.color.RGBAColorValue; -import dev.loki.lovisual.config.values.color.RGBColorValue; -import dev.loki.lovisual.config.values.mode.BindMode; -import dev.loki.lovisual.config.values.mode.ModeValue; -import dev.loki.lovisual.config.values.primitive.BooleanValue; -import dev.loki.lovisual.config.values.primitive.NumberValue; -import dev.loki.lovisual.util.screen.ClientScreen; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.client.gui.screens.ChatScreen; -import net.minecraft.util.Util; -import net.minecraft.world.item.ItemStack; -import dev.loki.lovisual.config.definition.SettingDef; -import dev.loki.lovisual.features.gui.hud.registry.HudElementRegister; -import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; -import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; -import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; -import dev.loki.lovisual.render.engine.math.HudScale; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.text.font.info.FontInfo; -import dev.loki.lovisual.render.engine.text.font.Fonts; -import dev.loki.lovisual.render.engine.text.render.TextRenderer; -import dev.loki.lovisual.util.input.KeyManager; - -import java.util.ArrayList; -import java.util.List; - -import static dev.loki.lovisual.features.theme.Theme.theme; -import dev.loki.lovisual.features.gui.hud.draggable.impl.HudPanelLayoutModes; -import dev.loki.lovisual.features.gui.hud.draggable.impl.scripted.ScriptedInventoryHudPanel; -import dev.loki.lovisual.features.gui.hud.draggable.impl.scripted.ScriptedListHudPanel; - -@HudElementRegister(order = 220) -public final class Inventory extends DraggableHudElement { - - { - defaultLayout(1255.352f, 658.36005f); - } - - private static final int COLS = 9; - private static final int ROWS = 3; - private static final int FIRST_INV_SLOT = 9; - private static final int LAST_INV_SLOT = 36; - - private static final float BASE_WIDTH = 123.0f; - private static final float BASE_HEIGHT = 62.0f; - private static final float HEADER_HEIGHT = 15.5f; - private static final float BODY_Y_OFFSET = 18.4f; - private static final float BODY_HEIGHT = 45.0f; - private static final float PANEL_RADIUS = 4.0f; - private static final float PANEL_SOFTNESS = 1.0f; - private static final float PANEL_STROKE = 0.55f; - private static final float HEADER_DIVIDER_X = 18.0f; - private static final float HEADER_DIVIDER_Y = 5.0f; - private static final float HEADER_DIVIDER_H = 6.0f; - private static final float TITLE_ICON_X = 4.5f; - private static final float TITLE_ICON_Y = 6.0f; - private static final float TITLE_TEXT_X = 22.0f; - private static final float TITLE_TEXT_Y = 6.5f; - private static final float COUNT_LABEL_OFFSET = 21.0f; - private static final float COUNT_VALUE_OFFSET = 2.0f; - private static final float GRID_START_X = 4.0f; - private static final float GRID_START_Y = 22.0f; - private static final float GRID_STEP = 13.0f; - private static final float GRID_LINE_OFFSET_X = 11.0f; - private static final float GRID_LINE_OFFSET_Y = 10.0f; - private static final float GRID_LINE_LENGTH = 9.0f; - private static final float GRID_LINE_THICKNESS = 0.5f; - private static final float ITEM_RENDER_SCALE = 0.5f; - - private static final String COLOR_THEME = "Theme"; - private static final String COLOR_CUSTOM = "Custom"; - private static final String EFFECT_NONE = "None"; - private static final String EFFECT_BLUR = "Blur"; - - private final HudGlobalConfig hud = HudGlobalConfig.get(); - private final ScriptedInventoryHudPanel scriptedPanel = new ScriptedInventoryHudPanel(); - - private final KeyBindValue toggleBind = - new KeyBindValue("toggle_bind", "I"); - private final NumberValue scaleValue = - new NumberValue<>("inventory_scale", 1.32, HudPanelLayoutModes.SCALE_MIN, HudPanelLayoutModes.SCALE_MAX); - private final ModeValue layoutMode = - new ModeValue("inventory_layout", "Unified Divider", HudPanelLayoutModes.SPLIT_HEADER, HudPanelLayoutModes.UNIFIED_DIVIDER); - private final ModeValue colorMode = - new ModeValue("inventory_color_mode", "Theme", COLOR_THEME, COLOR_CUSTOM); - private final ModeValue bgEffect = - new ModeValue("inventory_bg_effect", "Blur", EFFECT_NONE, EFFECT_BLUR); - private final RGBAColorValue bg = - new RGBAColorValue("inventory_bg", "#F7343434"); - private final RGBAColorValue bg2 = - new RGBAColorValue("inventory_bg_secondary", "#F7161616"); - private final NumberValue bgAlpha = - new NumberValue<>("inventory_bg_alpha", 194, 0, 255); - private final RGBColorValue stroke = - new RGBColorValue("inventory_stroke", "#5A5A5A"); - private final RGBColorValue text = - new RGBColorValue("inventory_text", "#FFFFFF"); - private final RGBColorValue muted = - new RGBColorValue("inventory_muted", "#A5A5A5"); - private final NumberValue blurAlpha = - new NumberValue<>("inventory_blur_alpha", 255, 0, 255); - private final BooleanValue headerIconPulse = - new BooleanValue("inventory_header_icon_pulse", true); - private final NumberValue headerIconPulseSpeed = - new NumberValue<>("inventory_header_icon_pulse_speed", 15, 1, 60); - private final NumberValue headerIconPulseIntensity = - new NumberValue<>("inventory_header_icon_pulse_intensity", 100, 0, 100); - - private final List pendingItemTasks = new ArrayList<>(COLS * ROWS); - - private float displayWidth = -1.0f; - private float displayHeight = -1.0f; - private float visibilityAnim = 0.0f; - private int uiHeaderLeft; - private int uiHeaderRight; - private int uiBodyLeft; - private int uiBodyRight; - private int uiOutline; - private int uiText; - private int uiMuted; - private int uiCounter; - private int uiTitleText; - private int uiDivider; - private int uiGridDivider; - private int uiBlurTint; - private boolean layoutReady; - private boolean hitboxActive; - private boolean displayVisible = true; - private boolean toggleArmed = true; - private boolean toggleInit = false; - - public Inventory() { - super("inventory", "Inventory", true); - } - - @Override - protected void defineSettings(List defs) { - defs.add(SettingDef.bind(toggleBind, BindMode.PRESS)); - defs.add(SettingDef.number(scaleValue)); - defs.add(SettingDef.mode(layoutMode)); - defs.add(SettingDef.mode(colorMode)); - defs.add(SettingDef.color(bg).visibleWhen(this::isCustomMode)); - defs.add(SettingDef.color(bg2).visibleWhen(this::isCustomMode)); - defs.add(SettingDef.colorNoAlpha(stroke).visibleWhen(this::isCustomMode)); - defs.add(SettingDef.number(bgAlpha).visibleWhen(this::isThemeMode)); - defs.add(SettingDef.colorNoAlpha(text).visibleWhen(this::isCustomMode)); - defs.add(SettingDef.colorNoAlpha(muted).visibleWhen(this::isCustomMode)); - defs.add(SettingDef.mode(bgEffect)); - defs.add(SettingDef.number(blurAlpha).visibleWhen(this::hasEffect)); - defs.add(SettingDef.bool(headerIconPulse)); - defs.add(SettingDef.number(headerIconPulseSpeed).visibleWhen(headerIconPulse::get)); - defs.add(SettingDef.number(headerIconPulseIntensity).visibleWhen(headerIconPulse::get)); - } - - @Override - public void applyDefaultPosition(int screenW, int screenH) { - this.x = 385.0f; - this.y = 40.0f; - } - - @Override - public boolean usesEngineRenderer() { - return true; - } - - @Override - public void onTick() { - Minecraft mc = Minecraft.getInstance(); - if (!toggleInit) { - if (mc == null || mc.getWindow() == null) { - return; - } - initToggleState(); - toggleInit = true; - } - - String combo = toggleBind.get(); - if (combo == null || combo.isBlank() || "NONE".equalsIgnoreCase(combo)) { - return; - } - if (mc == null || mc.getWindow() == null) { - return; - } - - boolean pressed = ClientScreen.current() == null && KeyManager.isComboHeldAllowScreen(combo); - if (!pressed) { - toggleArmed = true; - return; - } - if (toggleArmed) { - displayVisible = !displayVisible; - toggleArmed = false; - } - } - - @Override - public void renderEngine(Renderer2D renderer, - TextRenderer textRenderer, - GuiGraphicsExtractor ctx, - float tickDelta, - int screenW, - int screenH) { - Minecraft mc = Minecraft.getInstance(); - layoutReady = false; - hitboxActive = false; - pendingItemTasks.clear(); - - boolean forceVisible = DraggableHudElementRegistry.isForceVisible(); - if (mc == null || (mc.player == null && !forceVisible)) { - width = 0.0f; - height = 0.0f; - return; - } - - if (!isEnabled() && !forceVisible) { - width = 0.0f; - height = 0.0f; - return; - } - - boolean chatPreview = ClientScreen.current() instanceof ChatScreen; - List stacks = collectStacks(); - boolean hasAnyItems = stacks.stream().anyMatch(stack -> stack != null && !stack.isEmpty()); - boolean displayTargetVisible = displayVisible; - boolean showWidget = forceVisible || (displayTargetVisible && (chatPreview || hasAnyItems)); - visibilityAnim = HudRenderUtil.animateVisibility(visibilityAnim, showWidget); - float widgetScale = HudRenderUtil.visibilityScale(visibilityAnim); - if (!showWidget && visibilityAnim <= 0.0f) { - width = 0.0f; - height = 0.0f; - return; - } - - updatePalette(); - - TextRenderer headerIconRenderer = Fonts.renderer("IconsNur", FontInfo.Type.Regular, TextRenderer.get()); - TextRenderer headerTextRenderer = Fonts.renderer("Inter", FontInfo.Type.Bold, TextRenderer.get()); - TextRenderer countRenderer = Fonts.renderer("OnestMedium", FontInfo.Type.Regular, TextRenderer.get()); - if (headerIconRenderer == null) headerIconRenderer = textRenderer; - if (headerTextRenderer == null) headerTextRenderer = textRenderer; - if (countRenderer == null) countRenderer = textRenderer; - - float baseScale = HudScale.scale(screenW, screenH) * 1.1f * HudPanelLayoutModes.effectiveScale(scaleValue); - float fontScale = 0.98f * (hud.getFontSize() / 18.0f); - long itemCount = 0L; - for (ItemStack stack : stacks) { - if (stack != null && !stack.isEmpty()) { - itemCount += stack.getCount(); - } - } - - countRenderer.begin(fontScale * 0.92f, false, false); - String itemCountText = Long.toString(Math.max(0L, itemCount)); - float countLabelWidth = (float) countRenderer.getWidth("Items:", false); - float countValueWidth = (float) countRenderer.getWidth(itemCountText, false); - float countTextHeight = (float) countRenderer.getHeight(false); - countRenderer.end(); - - headerIconRenderer.begin(fontScale, false, false); - headerTextRenderer.begin(fontScale, false, false); - float headerIconH = (float) headerIconRenderer.getHeight(false); - float headerTextH = (float) headerTextRenderer.getHeight(false); - float titleWidth = (float) headerTextRenderer.getWidth("Inventory", false); - headerTextRenderer.end(); - headerIconRenderer.end(); - - float targetWidth = BASE_WIDTH * baseScale; - float headerWidth = (TITLE_TEXT_X * baseScale) + titleWidth - + countLabelWidth + countValueWidth - + ((COUNT_LABEL_OFFSET + 12.0f) * baseScale); - targetWidth = Math.max(targetWidth, headerWidth); - float targetHeight = BASE_HEIGHT * baseScale; - - displayWidth = HudRenderUtil.animateDimension(displayWidth, targetWidth); - displayHeight = HudRenderUtil.animateDimension(displayHeight, targetHeight); - width = displayWidth; - height = displayHeight; - - float drawScale = Math.max(0.0f, widgetScale); - float drawWidth = displayWidth * drawScale; - float drawHeight = displayHeight * drawScale; - if (drawWidth <= 0.0f || drawHeight <= 0.0f) { - width = 0.0f; - height = 0.0f; - return; - } - - float drawX = x + ((displayWidth - drawWidth) * 0.5f); - float drawY = y + ((displayHeight - drawHeight) * 0.5f); - float drawBaseScale = baseScale * drawScale; - float drawFontScale = fontScale * drawScale; - float drawHeaderIconH = headerIconH * drawScale; - float drawHeaderTextH = headerTextH * drawScale; - float drawCountTextHeight = countTextHeight * drawScale; - float drawCountLabelWidth = countLabelWidth * drawScale; - float drawCountValueWidth = countValueWidth * drawScale; - hitboxActive = forceVisible || showWidget; - - int resolvedHeaderIconColor = uiCounter; - if (headerIconPulse.get()) { - int pulsedHeaderIconColor = HudTextEffects.animatedColor( - uiCounter, - HudTextEffects.Effect.PULSE, - headerIconPulseSpeed.get(), - (float) (Util.getMillis() / 1000.0), - 0.0f - ); - resolvedHeaderIconColor = HudRenderUtil.mixColor( - uiCounter, - pulsedHeaderIconColor, - headerIconPulseIntensity.get() / 100.0f - ); - } - - scriptedPanel.render( - renderer, - textRenderer, - ctx, - tickDelta, - new ScriptedInventoryHudPanel.Panel( - drawX, - drawY, - drawWidth, - drawHeight, - drawScale, - drawBaseScale, - drawFontScale, - drawHeaderIconH, - drawHeaderTextH, - drawCountTextHeight, - drawCountLabelWidth, - drawCountValueWidth, - itemCount, - hasEffect(), - Math.min(1.0f, (blurAlpha.get() / 255.0f) * (isThemeMode() ? 1.15f : 1.0f)), - resolvedHeaderIconColor, - uiGridDivider, - HudPanelLayoutModes.current(layoutMode), - new ScriptedListHudPanel.Palette(uiHeaderLeft, uiHeaderRight, uiBodyLeft, uiBodyRight, - uiOutline, uiText, uiMuted, uiCounter, uiTitleText, uiDivider, uiBlurTint), - ScriptedInventoryHudPanel.cells(stacks) - ) - ); - } - - @Override - public void renderEngineForeground(Renderer2D renderer, - TextRenderer textRenderer, - GuiGraphicsExtractor ctx, - float tickDelta, - int screenW, - int screenH) { - if (!layoutReady || ctx == null || pendingItemTasks.isEmpty()) { - pendingItemTasks.clear(); - return; - } - for (QueuedItemIcon task : pendingItemTasks) { - // Inventory is rendered in UNSCALED_LOGICAL space, so item() must receive - // logical coordinates directly. itemUnscaled() would shift icons out of slots. - renderer.item(task.stack(), task.x(), task.y(), task.scale(), task.seed(), Renderer2D.ITEM_OVERLAY_ALL, null); - } - pendingItemTasks.clear(); - } - - @Override - public boolean contains(float mx, float my) { - if (!hitboxActive && !DraggableHudElementRegistry.isForceVisible()) { - return false; - } - return super.contains(mx, my); - } - - private List collectStacks() { - Minecraft mc = Minecraft.getInstance(); - List stacks = new ArrayList<>(LAST_INV_SLOT - FIRST_INV_SLOT); - if (mc == null || mc.player == null) { - return stacks; - } - for (int slot = FIRST_INV_SLOT; slot < LAST_INV_SLOT; slot++) { - stacks.add(mc.player.getInventory().getItem(slot)); - } - return stacks; - } - - private void updatePalette() { - if (isThemeMode()) { - int alpha = bgAlpha.get(); - int window = HudRenderUtil.setAlpha(theme().windowBg(), alpha); - int header = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme().windowHeader(), theme().surface(), 0.18f), - Math.min(255, alpha + 14) - ); - int surface = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme().surface(), theme().windowBg(), 0.22f), - Math.min(255, alpha + 6) - ); - int deep = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme().surface(), theme().windowHeader(), 0.42f), - Math.min(255, alpha + 18) - ); - - uiHeaderLeft = HudRenderUtil.mixColor(header, window, 0.22f); - uiHeaderRight = HudRenderUtil.mixColor(surface, header, 0.52f); - uiBodyLeft = HudRenderUtil.mixColor(window, surface, 0.24f); - uiBodyRight = HudRenderUtil.mixColor(deep, surface, 0.18f); - uiOutline = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme().windowStroke(), theme().strokeSoft(), 0.18f), - Math.min(255, Math.max(182, alpha + 36)) - ); - uiText = theme().textPrimary(); - uiTitleText = HudRenderUtil.mixColor(theme().textPrimary(), theme().accent(), 0.14f); - uiMuted = HudRenderUtil.mixColor(theme().textMuted(), theme().textPrimary(), 0.16f); - uiCounter = HudRenderUtil.mixColor(theme().accent(), theme().textPrimary(), 0.38f); - uiDivider = HudRenderUtil.scaleAlpha(uiMuted, 0.68f); - uiGridDivider = HudRenderUtil.scaleAlpha(uiMuted, 0.28f); - uiBlurTint = HudRenderUtil.mixColor( - HudRenderUtil.mixColor(uiHeaderRight, uiBodyRight, 0.5f), - theme().accent(), - 0.14f - ); - return; - } - - uiHeaderLeft = bg.getArgb(); - uiHeaderRight = bg2.getArgb(); - uiBodyLeft = HudRenderUtil.mixColor(bg.getArgb(), bg2.getArgb(), 0.15f); - uiBodyRight = bg2.getArgb(); - uiOutline = stroke.getArgb(); - uiText = text.getArgb(); - uiTitleText = uiText; - uiMuted = muted.getArgb(); - uiCounter = HudRenderUtil.mixColor(text.getArgb(), 0xFFE1E1FF, 0.20f); - uiDivider = HudRenderUtil.scaleAlpha(uiMuted, 0.55f); - uiGridDivider = HudRenderUtil.scaleAlpha(uiMuted, 0.20f); - uiBlurTint = HudRenderUtil.mixColor(uiHeaderLeft, uiBodyRight, 0.5f); - } - - private boolean isThemeMode() { - return COLOR_THEME.equals(colorMode.get()); - } - - private boolean isCustomMode() { - return COLOR_CUSTOM.equals(colorMode.get()); - } - - private boolean hasEffect() { - return !EFFECT_NONE.equals(bgEffect.get()); - } - - private void initToggleState() { - Minecraft mc = Minecraft.getInstance(); - toggleArmed = true; - String combo = toggleBind.get(); - if (combo != null && !combo.isBlank() && !"NONE".equalsIgnoreCase(combo) - && mc != null && ClientScreen.current() == null - && KeyManager.isComboHeldAllowScreen(combo)) { - toggleArmed = false; - } - } - - private record QueuedItemIcon(ItemStack stack, float x, float y, float scale, int seed) { - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/Itemizer.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/Itemizer.java deleted file mode 100644 index 3fae2d3..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/Itemizer.java +++ /dev/null @@ -1,699 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.features.gui.hud.draggable.impl.combat; - -import dev.loki.lovisual.config.values.primitive.BooleanValue; -import dev.loki.lovisual.config.values.mode.EnumValue; -import dev.loki.lovisual.config.values.primitive.NumberValue; -import dev.loki.lovisual.features.gui.hud.anchor.HudAnchorX; -import dev.loki.lovisual.features.gui.hud.anchor.HudAnchorY; -import dev.loki.lovisual.features.gui.hud.registry.HudElementRegister; -import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; -import dev.loki.lovisual.features.gui.hud.script.HudScriptLayouts; -import dev.loki.lovisual.features.module.phase.HudPhase; -import dev.loki.lovisual.features.theme.Theme; -import dev.loki.lovisual.features.theme.Themes; -import dev.loki.lovisual.render.engine.animation.AnimationUtility; -import dev.loki.lovisual.render.engine.math.ColorMath; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.runtime.UiRuntime; -import dev.loki.lovisual.render.engine.renderer.ui.runtime.render.context.UiProjectionMode; -import dev.loki.lovisual.render.engine.renderer.ui.runtime.render.context.UiRenderContext; -import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.runtime.CachedUiScriptRuntime; -import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.module.UiScriptModule; -import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.module.UiScriptModuleHandle; -import dev.loki.lovisual.render.engine.text.render.TextRenderer; -import dev.loki.lovisual.render.helpers.clip.ClipFunction; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.Items; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -@HudElementRegister(order = 200) -public final class Itemizer extends DraggableHudElement { - private static final String LAYOUT_ID = "lovisual:api/hud/draggable/itemizer"; - private static final float BASE_ICON_CARD = 24.0f; - private static final float BASE_COMPACT_W = 72.0f; - private static final float BASE_COMPACT_H = 24.0f; - private static final float BASE_GAP = 4.0f; - private static final float DEFAULT_DURATION = 1.15f; - private static final int MAX_INTERNAL_ENTRIES = 8; - - public static Itemizer INSTANCE; - - private final UiScriptModuleHandle moduleHandle = HudScriptLayouts.handle(LAYOUT_ID); - private final CachedUiScriptRuntime runtime = new CachedUiScriptRuntime(HudScriptLayouts.runtimeReporter()); - private final LinkedHashMap entries = new LinkedHashMap<>(); - private final List itemTasks = new ArrayList<>(5); - - private final NumberValue scale = - num("itemizer_scale", "scale", 1.0, 0.5, 3.0); - private final EnumValue displayMode = - enumSetting("itemizer_display_mode", "display_mode", ItemizerDisplayMode.ICONS, ItemizerDisplayMode.values()); - private final EnumValue direction = - enumSetting("itemizer_direction", "direction", ItemizerDirection.HORIZONTAL, ItemizerDirection.values()); - private final NumberValue duration = - num("itemizer_duration", "duration", (double) DEFAULT_DURATION, 0.25, 3.0); - private final NumberValue maxEntries = - num("itemizer_max_entries", "max_entries", 3, 1, 5); - private final BooleanValue showAutoEat = - bool("itemizer_auto_eat", "auto_eat", true); - private final BooleanValue showAutoTotem = - bool("itemizer_auto_totem", "auto_totem", true); - private final BooleanValue showElytraSwap = - bool("itemizer_elytra_swap", "elytra_swap", true); - private final BooleanValue blur = - bool("itemizer_blur", "blur", true); - private final NumberValue bgAlpha = - num("itemizer_bg_alpha", "bg_alpha", 168, 0, 255); - - private float visibilityAnim; - private float displayWidth = -1.0f; - private float displayHeight = -1.0f; - private boolean foregroundReady; - - public Itemizer() { - super("itemizer", "Itemizer", true); - INSTANCE = this; - defaultLayout(-16.0f, 48.0f, "CENTER", "CENTER"); - } - - public static void showAutoEat(ItemStack stack) { - show(ItemizerEvent.AUTO_EAT, stack, "Eat"); - } - - public static void showAutoTotem(ItemStack stack) { - show(ItemizerEvent.AUTO_TOTEM, stack, "Totem"); - } - - public static void showElytraSwap(ItemStack stack) { - show(ItemizerEvent.ELYTRA_SWAP, stack, "Swap"); - } - - public static void hideElytraSwap() { - hide(ItemizerEvent.ELYTRA_SWAP); - } - - private static void show(ItemizerEvent event, ItemStack stack, String label) { - Itemizer itemizer = INSTANCE; - if (itemizer == null || event == null || stack == null || stack.isEmpty()) return; - itemizer.push(event, stack, label); - } - - private static void hide(ItemizerEvent event) { - Itemizer itemizer = INSTANCE; - if (itemizer == null || event == null) return; - itemizer.entries.remove(event); - } - - @Override - public void applyDefaultPosition(int screenW, int screenH) { - float w = baseEntryWidth() * scale.get().floatValue(); - float h = baseEntryHeight() * scale.get().floatValue(); - this.x = (screenW - w) * 0.5f; - this.y = screenH * 0.5f + 48.0f; - setAnchors(HudAnchorX.FREE, HudAnchorY.FREE, this.x, this.y); - } - - @Override - public boolean usesEngineRenderer() { - return true; - } - - @Override - public HudPhase getHudPhase() { - return HudPhase.LAST; - } - - @Override - public int getRenderOrder() { - return 82; - } - - @Override - public void renderEngine(Renderer2D renderer, - TextRenderer textRenderer, - GuiGraphicsExtractor ctx, - float tickDelta, - int screenW, - int screenH) { - foregroundReady = false; - itemTasks.clear(); - boolean forceVisible = DraggableHudElementRegistry.isForceVisible(); - if (!isEnabled() && !forceVisible) { - width = 0.0f; - height = 0.0f; - return; - } - - float dt = AnimationUtility.deltaTime(); - List visible = forceVisible && entries.isEmpty() - ? previewEntries() - : updateEntries(dt); - boolean showWidget = !visible.isEmpty(); - visibilityAnim = HudRenderUtil.animateVisibility(visibilityAnim, showWidget); - if (!showWidget && visibilityAnim <= 0.0f) { - width = 0.0f; - height = 0.0f; - return; - } - - float baseScale = scale.get().floatValue(); - int count = Math.max(1, visible.size()); - float entryW = baseEntryWidth() * baseScale; - float entryH = baseEntryHeight() * baseScale; - float gap = BASE_GAP * baseScale; - float targetWidth = direction.get() == ItemizerDirection.HORIZONTAL - ? entryW * count + gap * Math.max(0, count - 1) - : entryW; - float targetHeight = direction.get() == ItemizerDirection.HORIZONTAL - ? entryH - : entryH * count + gap * Math.max(0, count - 1); - - displayWidth = HudRenderUtil.animateDimension(displayWidth, targetWidth); - displayHeight = HudRenderUtil.animateDimension(displayHeight, targetHeight); - width = displayWidth; - height = displayHeight; - - float drawScale = HudRenderUtil.visibilityScale(visibilityAnim); - float drawWidth = displayWidth * drawScale; - float drawHeight = displayHeight * drawScale; - if (drawWidth <= 0.0f || drawHeight <= 0.0f) return; - - float drawX = x + (displayWidth - drawWidth) * 0.5f; - float drawY = y + (displayHeight - drawHeight) * 0.5f; - float drawBaseScale = baseScale * drawScale; - drawCards(renderer, visible, drawX, drawY, drawBaseScale, AnimationUtility.clamp01(visibilityAnim)); - - UiScriptModule module = ensureModule(); - if (module == null) return; - - TextRenderer fallback = textRenderer != null ? textRenderer : TextRenderer.get(); - LinkedHashMap props = props(visible, drawWidth, drawHeight, drawBaseScale, drawScale); - long treeSignature = signature(props); - long layoutSignature = 0xcbf29ce484222325L; - layoutSignature = CachedUiScriptRuntime.mix(layoutSignature, drawX); - layoutSignature = CachedUiScriptRuntime.mix(layoutSignature, drawY); - layoutSignature = CachedUiScriptRuntime.mix(layoutSignature, drawWidth); - layoutSignature = CachedUiScriptRuntime.mix(layoutSignature, drawHeight); - - UiRuntime baked = runtime.bake( - moduleHandle, - module, - "itemizer", - treeSignature, - layoutSignature, - drawWidth, - drawHeight, - fallback, - drawX, - drawY, - drawWidth, - drawHeight, - () -> props - ); - if (baked == null) return; - baked.render(new UiRenderContext(renderer, fallback, ctx, tickDelta, UiProjectionMode.CURRENT)); - foregroundReady = true; - } - - @Override - public void renderEngineForeground(Renderer2D renderer, - TextRenderer textRenderer, - GuiGraphicsExtractor ctx, - float tickDelta, - int screenW, - int screenH) { - if (!foregroundReady || itemTasks.isEmpty()) { - itemTasks.clear(); - return; - } - int seed = 0; - for (ItemRenderTask task : itemTasks) { - drawItem(renderer, task.stack(), task.x(), task.y(), task.scale(), seed++, task.overlayFlags(), task.alpha()); - } - itemTasks.clear(); - foregroundReady = false; - } - - private void push(ItemizerEvent event, ItemStack stack, String label) { - if (!accepts(event)) return; - Entry entry = entries.remove(event); - if (entry == null) { - entry = new Entry(event); - } - entry.stack = stack.copy(); - entry.label = label != null && !label.isBlank() ? label : event.fallbackLabel; - entry.life = Math.max(0.1f, duration.get().floatValue()); - entry.totalLife = entry.life; - entry.punch = 1.0f; - entries.put(event, entry); - trimInternalEntries(); - } - - private boolean accepts(ItemizerEvent event) { - return switch (event) { - case AUTO_EAT -> showAutoEat.get(); - case AUTO_TOTEM -> showAutoTotem.get(); - case ELYTRA_SWAP -> showElytraSwap.get(); - }; - } - - private void trimInternalEntries() { - while (entries.size() > MAX_INTERNAL_ENTRIES) { - ItemizerEvent first = entries.keySet().iterator().next(); - entries.remove(first); - } - } - - private List updateEntries(float dt) { - List remove = new ArrayList<>(); - List out = new ArrayList<>(); - for (Map.Entry mapEntry : entries.entrySet()) { - Entry entry = mapEntry.getValue(); - entry.life = Math.max(0.0f, entry.life - dt); - entry.punch = AnimationUtility.approach(entry.punch, 0.0f, dt, 8.0f); - boolean alive = entry.life > 0.0f; - entry.anim = AnimationUtility.approach(entry.anim, alive ? 1.0f : 0.0f, dt, 13.0f); - entry.anim = AnimationUtility.snap(entry.anim, alive ? 1.0f : 0.0f, 0.002f); - if (!alive && entry.anim <= 0.0f) { - remove.add(mapEntry.getKey()); - continue; - } - if (out.size() < maxEntries.get()) { - out.add(entry); - } - } - for (ItemizerEvent event : remove) { - entries.remove(event); - } - return out; - } - - private List previewEntries() { - Entry eat = new Entry(ItemizerEvent.AUTO_EAT); - eat.stack = new ItemStack(Items.COOKED_BEEF); - eat.label = "Eat"; - eat.life = 1.0f; - eat.totalLife = 1.0f; - eat.anim = 1.0f; - - Entry totem = new Entry(ItemizerEvent.AUTO_TOTEM); - totem.stack = new ItemStack(Items.TOTEM_OF_UNDYING); - totem.label = "Totem"; - totem.life = 1.0f; - totem.totalLife = 1.0f; - totem.anim = 1.0f; - - Entry swap = new Entry(ItemizerEvent.ELYTRA_SWAP); - swap.stack = new ItemStack(Items.ELYTRA); - swap.label = "Swap"; - swap.life = 1.0f; - swap.totalLife = 1.0f; - swap.anim = 1.0f; - - List list = new ArrayList<>(); - list.add(eat); - list.add(totem); - list.add(swap); - return list.subList(0, Math.min(maxEntries.get(), list.size())); - } - - private UiScriptModule ensureModule() { - Minecraft mc = Minecraft.getInstance(); - if (mc == null || mc.getResourceManager() == null) return null; - HudScriptLayouts.pollReloadCombo(mc); - if (moduleHandle.consumeChanged()) { - runtime.reset(); - } - if (!moduleHandle.ensureLoaded(mc.getResourceManager())) { - HudScriptLayouts.reportLoadError(moduleHandle); - return null; - } - moduleHandle.consumeChanged(); - return moduleHandle.module(); - } - - private LinkedHashMap props(List visible, - float width, - float height, - float baseScale, - float drawScale) { - LinkedHashMap out = new LinkedHashMap<>(); - out.put("id", "itemizer"); - out.put("width", width); - out.put("height", height); - out.put("baseScale", baseScale); - out.put("drawScale", drawScale); - out.put("mode", displayMode.get().id()); - out.put("direction", direction.get().id()); - out.put("entryWidth", baseEntryWidth() * baseScale); - out.put("entryHeight", baseEntryHeight() * baseScale); - out.put("gap", BASE_GAP * baseScale); - out.put("blur", blur.get()); - out.put("palette", paletteProps()); - List> itemProps = new ArrayList<>(); - for (Entry entry : visible) { - itemProps.add(entryProps(entry)); - } - out.put("items", itemProps.toArray()); - return out; - } - - private LinkedHashMap paletteProps() { - Themes.Theme t = Theme.theme(); - int alpha = bgAlpha.get(); - int bg1 = HudRenderUtil.setAlpha(HudRenderUtil.mixColor(t.windowBg(), t.surface(), 0.24f), alpha); - int bg2 = HudRenderUtil.setAlpha(HudRenderUtil.mixColor(t.surface(), 0xFF000000, 0.22f), Math.min(255, alpha + 18)); - int stroke = HudRenderUtil.setAlpha(HudRenderUtil.mixColor(t.windowStroke(), t.accentSoft(), 0.22f), 176); - LinkedHashMap out = new LinkedHashMap<>(); - out.put("bg1", hex(bg1)); - out.put("bg2", hex(bg2)); - out.put("stroke", hex(stroke)); - out.put("text", hex(t.textPrimary())); - out.put("muted", hex(t.textMuted())); - out.put("accent", hex(t.accent())); - out.put("glow", hex(HudRenderUtil.setAlpha(t.accent(), 155))); - out.put("blurAlpha", Math.min(1.0f, alpha / 255.0f)); - return out; - } - - private LinkedHashMap entryProps(Entry entry) { - ItemStack stack = entry.stack != null ? entry.stack : ItemStack.EMPTY; - LinkedHashMap out = new LinkedHashMap<>(); - out.put("key", entry.event.id()); - out.put("kind", entry.event.id()); - out.put("label", entry.label); - out.put("stack", stack.copy()); - out.put("item", stack.isEmpty() ? "" : BuiltInRegistries.ITEM.getKey(stack.getItem()).toString()); - out.put("count", stack.isEmpty() ? 1 : stack.getCount()); - out.put("damage", stack.isEmpty() ? 0 : stack.getDamageValue()); - out.put("maxDamage", stack.isEmpty() ? 0 : stack.getMaxDamage()); - out.put("alpha", AnimationUtility.clamp01(entry.anim)); - out.put("life", entry.totalLife <= 0.0f ? 1.0f : AnimationUtility.clamp01(entry.life / entry.totalLife)); - out.put("pulse", AnimationUtility.clamp01(entry.punch)); - out.put("accent", hex(entryAccent(entry.event))); - return out; - } - - private int entryAccent(ItemizerEvent event) { - Themes.Theme t = Theme.theme(); - int target = switch (event) { - case AUTO_EAT -> 0xFF74E083; - case AUTO_TOTEM -> 0xFFFFD15C; - case ELYTRA_SWAP -> 0xFF77D7FF; - }; - return ColorMath.colorWithAlpha(HudRenderUtil.mixColor(t.accent(), target, 0.72f), 255); - } - - private void drawCards(Renderer2D renderer, - List visible, - float x, - float y, - float scale, - float globalAlpha) { - if (renderer == null || visible == null || visible.isEmpty()) return; - float entryW = baseEntryWidth() * scale; - float entryH = baseEntryHeight() * scale; - float gap = BASE_GAP * scale; - for (int i = 0; i < visible.size(); i++) { - Entry entry = visible.get(i); - float cardX = x; - float cardY = y; - if (direction.get() == ItemizerDirection.HORIZONTAL) { - cardX += i * (entryW + gap); - } else { - cardY += i * (entryH + gap); - } - - float alpha = AnimationUtility.clamp01(globalAlpha * entry.anim); - if (alpha <= 0.001f) continue; - float radius = Math.max(3.0f * scale, Math.min(entryW, entryH) * 0.24f); - drawGlassCard(renderer, cardX, cardY, entryW, entryH, radius, scale, alpha); - drawClippedTimeStrip(renderer, entry, cardX, cardY, entryW, entryH, radius, scale, alpha); - queueItem(entry, cardX, cardY, entryW, entryH, scale, alpha); - } - } - - private void drawGlassCard(Renderer2D renderer, - float x, - float y, - float width, - float height, - float radius, - float scale, - float alpha) { - if (width <= 0.0f || height <= 0.0f || alpha <= 0.001f) return; - float blurStrength = blur.get() ? (bgAlpha.get() / 255.0f) * alpha : 0.0f; - HudRenderUtil.drawLiquidGlass(x, y, width, height, radius, Math.max(0.001f, scale), true, blurStrength, alpha); - int veil = HudRenderUtil.glassSmallBackground(alpha); - renderer.roundedRectCorners(x, y, width, height, radius, radius, radius, radius, 1.0f, veil); - } - - private void drawClippedTimeStrip(Renderer2D renderer, - Entry entry, - float x, - float y, - float width, - float height, - float cardRadius, - float scale, - float alpha) { - boolean clipped = ClipFunction.pushRoundedRect(x, y, width, height, cardRadius); - if (!clipped) return; - try { - drawTimeStrip(renderer, entry, x, y, width, height, cardRadius, scale, alpha); - } finally { - ClipFunction.pop(); - } - } - - private void drawTimeStrip(Renderer2D renderer, - Entry entry, - float x, - float y, - float width, - float height, - float cardRadius, - float scale, - float alpha) { - if (renderer == null || entry == null || width <= 0.0f || height <= 0.0f || alpha <= 0.001f) return; - float life = entry.totalLife <= 0.0f ? 1.0f : AnimationUtility.clamp01(entry.life / entry.totalLife); - if (life <= 0.001f) return; - - float stripH = Math.max(1.45f * scale, Math.min(3.0f * scale, height * 0.13f)); - float bleed = Math.max(0.65f, 0.45f * scale); - float stripDrawH = stripH + bleed; - float stripY = y + height - stripH; - float bottomR = Math.min(cardRadius, stripDrawH); - int accent = entryAccent(entry.event); - int trackTop = HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(0xFF07090D, accent, 0.08f), 0.30f * alpha); - int trackBottom = HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(0xFF020305, accent, 0.18f), 0.48f * alpha); - renderer.roundedRectCornersQuad( - x, - stripY, - width, - stripDrawH, - 0.0f, - 0.0f, - bottomR, - bottomR, - 0.0f, - trackTop, - trackTop, - trackBottom, - trackBottom - ); - - float fillW = Math.max(0.0f, width * life); - if (fillW <= 0.35f) return; - float fillRLeft = Math.min(bottomR, fillW * 0.5f); - float fillRRight = life >= 0.985f ? bottomR : Math.min(stripDrawH * 0.5f, fillW * 0.5f); - int fillTopLeft = HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(accent, 0xFFFFFFFF, 0.28f), 0.92f * alpha); - int fillTopRight = HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(accent, 0xFFFFFFFF, 0.10f), 0.84f * alpha); - int fillBottomRight = HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(accent, 0xFF05070A, 0.22f), 0.78f * alpha); - int fillBottomLeft = HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(accent, 0xFFFFFFFF, 0.16f), 0.88f * alpha); - renderer.roundedRectCornersQuad( - x, - stripY, - fillW, - stripDrawH, - 0.0f, - life >= 0.985f ? 0.0f : fillRRight, - fillRRight, - fillRLeft, - 0.0f, - fillTopLeft, - fillTopRight, - fillBottomRight, - fillBottomLeft - ); - } - - private void queueItem(Entry entry, - float cardX, - float cardY, - float cardW, - float cardH, - float scale, - float alpha) { - if (entry == null || entry.stack == null || entry.stack.isEmpty()) return; - boolean compact = displayMode.get() == ItemizerDisplayMode.COMPACT; - float iconSize = compact - ? Math.min(16.0f * scale, cardH - 7.0f * scale) - : Math.min(Math.min(16.0f * scale, cardW - 8.0f * scale), cardH - 8.0f * scale); - if (iconSize <= 0.0f) return; - float iconX = compact ? cardX + 4.5f * scale : cardX + (cardW - iconSize) * 0.5f; - float iconY = cardY + (cardH - iconSize) * 0.5f - 0.5f * scale; - int overlayFlags = compact ? Renderer2D.ITEM_OVERLAY_ALL : Renderer2D.ITEM_OVERLAY_NONE; - itemTasks.add(new ItemRenderTask(entry.stack.copy(), iconX, iconY, Math.max(0.05f, iconSize / 16.0f), overlayFlags, alpha)); - } - - private void drawItem(Renderer2D renderer, - ItemStack stack, - float x, - float y, - float itemScale, - int seed, - int overlayFlags, - float alpha) { - if (renderer == null || stack == null || stack.isEmpty() || alpha <= 0.001f) return; - double previousAlpha = renderer.getAlpha(); - renderer.setAlpha(previousAlpha * AnimationUtility.clamp01(alpha)); - try { - renderer.item(stack, x, y, itemScale, seed, overlayFlags, null); - } finally { - renderer.setAlpha(previousAlpha); - } - } - - private long signature(Map props) { - long h = 0xcbf29ce484222325L; - h = CachedUiScriptRuntime.mix(h, displayMode.get().id()); - h = CachedUiScriptRuntime.mix(h, direction.get().id()); - h = CachedUiScriptRuntime.mix(h, width); - h = CachedUiScriptRuntime.mix(h, height); - h = CachedUiScriptRuntime.mix(h, bgAlpha.get()); - h = CachedUiScriptRuntime.mix(h, blur.get()); - Object itemsValue = props.get("items"); - Object[] items = itemsValue instanceof Object[] arr ? arr : new Object[0]; - h = CachedUiScriptRuntime.mix(h, items.length); - for (Object itemValue : items) { - if (!(itemValue instanceof Map item)) continue; - h = CachedUiScriptRuntime.mix(h, string(item.get("key"))); - h = CachedUiScriptRuntime.mix(h, string(item.get("label"))); - h = CachedUiScriptRuntime.mix(h, string(item.get("item"))); - h = CachedUiScriptRuntime.mix(h, intValue(item.get("count"))); - h = CachedUiScriptRuntime.mix(h, Math.round(floatValue(item.get("alpha")) * 1000.0f)); - h = CachedUiScriptRuntime.mix(h, Math.round(floatValue(item.get("life")) * 1000.0f)); - h = CachedUiScriptRuntime.mix(h, Math.round(floatValue(item.get("pulse")) * 1000.0f)); - } - return h; - } - - private float baseEntryWidth() { - return displayMode.get() == ItemizerDisplayMode.COMPACT ? BASE_COMPACT_W : BASE_ICON_CARD; - } - - private float baseEntryHeight() { - return displayMode.get() == ItemizerDisplayMode.COMPACT ? BASE_COMPACT_H : BASE_ICON_CARD; - } - - private static String hex(int argb) { - return String.format("#%08X", argb); - } - - private static String string(Object value) { - return value instanceof String s ? s : ""; - } - - private static int intValue(Object value) { - return value instanceof Number n ? n.intValue() : 0; - } - - private static float floatValue(Object value) { - return value instanceof Number n ? n.floatValue() : 0.0f; - } - - public enum ItemizerEvent implements EnumValue.IdProvider { - AUTO_EAT("auto_eat", "Eat"), - AUTO_TOTEM("auto_totem", "Totem"), - ELYTRA_SWAP("elytra_swap", "Swap"); - - private final String id; - private final String fallbackLabel; - - ItemizerEvent(String id, String fallbackLabel) { - this.id = id; - this.fallbackLabel = fallbackLabel; - } - - @Override - public String id() { - return id; - } - } - - private enum ItemizerDisplayMode implements EnumValue.IdProvider { - ICONS("icons"), - COMPACT("compact"); - - private final String id; - - ItemizerDisplayMode(String id) { - this.id = id; - } - - @Override - public String id() { - return id; - } - } - - private enum ItemizerDirection implements EnumValue.IdProvider { - HORIZONTAL("horizontal"), - VERTICAL("vertical"); - - private final String id; - - ItemizerDirection(String id) { - this.id = id; - } - - @Override - public String id() { - return id; - } - } - - private static final class Entry { - private final ItemizerEvent event; - private ItemStack stack = ItemStack.EMPTY; - private String label; - private float life; - private float totalLife; - private float anim; - private float punch; - - private Entry(ItemizerEvent event) { - this.event = event; - this.label = event.fallbackLabel; - } - } - - private record ItemRenderTask(ItemStack stack, float x, float y, float scale, int overlayFlags, float alpha) { - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/MediaPlayer.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/MediaPlayer.java deleted file mode 100644 index 01b6b63..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/MediaPlayer.java +++ /dev/null @@ -1,743 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.features.gui.hud.draggable.impl.combat; - -import dev.loki.lovisual.config.values.color.RGBAColorValue; -import dev.loki.lovisual.config.values.color.RGBColorValue; -import dev.loki.lovisual.config.values.mode.ModeValue; -import dev.loki.lovisual.config.values.primitive.BooleanValue; -import dev.loki.lovisual.config.values.primitive.NumberValue; -import dev.loki.lovisual.util.screen.ClientScreen; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.client.gui.screens.ChatScreen; -import net.minecraft.client.resources.language.I18n; -import net.minecraft.resources.Identifier; -import net.minecraft.util.Util; -import dev.loki.lovisual.config.definition.SettingDef; -import dev.loki.lovisual.events.impl.render.RenderPrewarmCollectEvent; -import dev.loki.lovisual.features.gui.hud.registry.HudElementRegister; -import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; -import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; -import dev.loki.lovisual.render.engine.color.RenderColor; -import dev.loki.lovisual.render.engine.math.HudScale; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.svg.model.SvgRenderOptions; -import dev.loki.lovisual.render.engine.text.font.info.FontInfo; -import dev.loki.lovisual.render.engine.text.font.Fonts; -import dev.loki.lovisual.render.engine.text.render.TextRenderer; -import dev.loki.lovisual.util.media.info.MediaInfo; -import dev.loki.lovisual.util.media.session.MediaSessionService; -import dev.loki.lovisual.util.media.info.RepeatMode; - -import java.util.List; - -import static dev.loki.lovisual.features.theme.Theme.theme; - -@HudElementRegister(order = 70) -public final class MediaPlayer extends DraggableHudElement { - - { - defaultLayout(12.0f, 82.30222f); - } - - private static final float BASE_PADDING = 6f; - private static final float BASE_GAP = 6f; - private static final float BASE_ART_SIZE = 30f; - private static final float BASE_ART_BOOST = 3f; - private static final float BASE_ART_OVERHANG = 4f; - private static final float BASE_RADIUS = 6f; - private static final float BASE_BAR_H = 2.4f; - private static final float BASE_BUTTON_SIZE = 16f; - private static final float BASE_BUTTON_GAP = 8f; - private static final float BASE_TEXT_GAP = 2f; - private static final float BASE_BAR_GAP = 4f; - private static final float BASE_BOTTOM_GAP = 4f; - private static final float BASE_TITLE_SCROLL_SPEED = 22f; - private static final float BASE_TITLE_SCROLL_GAP = 14f; - private static final float BASE_TITLE_FADE = 12f; - private static final float TITLE_SCROLL_PAUSE_SEC = 0.8f; - private static final String EFFECT_NONE = "None"; - private static final String EFFECT_BLUR = "Blur"; - private static final String EFFECT_GLASS = "Glass"; - - private static final float BASE_CONTENT_W = 170f; - - private static final int ICON_PAUSE = 0xEA02; - private static final int ICON_PLAY = 0xEA03; - private static final int ICON_PREV = 0xEA04; - private static final int ICON_NEXT = 0xEA05; - private static final int ICON_SHUFFLE = 0xEA06; - - private final HudGlobalConfig hud = HudGlobalConfig.get(); - private final MediaSessionService mediaService = MediaSessionService.get(); - private final NumberValue scale = - new NumberValue<>("media_player_scale", 1.51, 0.5, 5.0); - private final BooleanValue syncTheme = - new BooleanValue("media_player_sync_theme", true); - private final RGBAColorValue bg = - new RGBAColorValue("media_player_bg", "#60000000"); - private final NumberValue bgAlpha = - new NumberValue<>("media_player_bg_alpha", 124, 0, 255); - private final RGBColorValue accent = - new RGBColorValue("media_player_accent", "#FF2F2F"); - private final RGBColorValue text = - new RGBColorValue("media_player_text", "#FFFFFF"); - private final ModeValue bgEffect = - new ModeValue("media_player_bg_effect", "None", EFFECT_NONE, EFFECT_BLUR, EFFECT_GLASS); - private final NumberValue blurAlpha = - new NumberValue<>("media_player_blur_alpha", 255, 0, 255); - private final BooleanValue gradient = - new BooleanValue("media_player_gradient", true); - private float anim = 0f; - private MediaSessionService.Snapshot currentSnapshot = MediaSessionService.Snapshot.empty(MediaSessionService.isMediaAvailable()); - - private float playHover; - private float shuffleHover; - private float prevHover; - private float nextHover; - private float repeatHover; - - private float shuffleX; - private float shuffleY; - private float playX; - private float playY; - private float prevX; - private float prevY; - private float nextX; - private float nextY; - private float repeatX; - private float repeatY; - private float buttonSize; - private int uiAccent; - private int uiAccentSoft; - private int uiText; - private int uiTextMuted; - - public MediaPlayer() { - super("media_player", "Media Player", false); - } - - private static String iconString(int codepoint) { - return new String(Character.toChars(codepoint)); - } - - private static TextRenderer getMediaIcons(TextRenderer fallback) { - return Fonts.renderer("MediaPlayer", FontInfo.Type.Regular, fallback); - } - - public void onResourceReload() { - mediaService.onResourceReload(); - currentSnapshot = MediaSessionService.Snapshot.empty(MediaSessionService.isMediaAvailable()); - } - - @Override - protected void defineSettings(List defs) { - defs.add(SettingDef.number(scale)); - defs.add(SettingDef.bool(syncTheme)); - defs.add(SettingDef.color(bg).visibleWhen(() -> !syncTheme.get() && !isGlassEffect())); - defs.add(SettingDef.number(bgAlpha).visibleWhen(() -> syncTheme.get() && !isGlassEffect())); - defs.add(SettingDef.colorNoAlpha(accent).visibleWhen(() -> !syncTheme.get())); - defs.add(SettingDef.colorNoAlpha(text).visibleWhen(() -> !syncTheme.get())); - defs.add(SettingDef.mode(bgEffect)); - defs.add(SettingDef.number(blurAlpha).visibleWhen(this::hasEffect)); - defs.add(SettingDef.bool(gradient).visibleWhen(() -> syncTheme.get() && !isGlassEffect())); - } - - @Override - public void applyDefaultPosition(int screenW, int screenH) { - float scale = HudScale.scale(screenW, screenH) * this.scale.get().floatValue(); - this.x = 20f * scale; - this.y = 20f * scale; - } - - @Override - public boolean usesEngineRenderer() { - return true; - } - - @Override - public boolean isMouseOverInteractive(float mx, float my) { - if (!(ClientScreen.current() instanceof ChatScreen)) return false; - if (buttonSize <= 0f) return false; - boolean showShuffle = currentSnapshot != null && currentSnapshot.supportsShuffle(); - boolean showRepeat = currentSnapshot != null && currentSnapshot.supportsRepeat(); - return hit(mx, my, playX, playY, buttonSize, buttonSize) - || (showShuffle && hit(mx, my, shuffleX, shuffleY, buttonSize, buttonSize)) - || hit(mx, my, prevX, prevY, buttonSize, buttonSize) - || hit(mx, my, nextX, nextY, buttonSize, buttonSize) - || (showRepeat && hit(mx, my, repeatX, repeatY, buttonSize, buttonSize)); - } - - @Override - public boolean onMouseClicked(float mx, float my, int button) { - if (!(ClientScreen.current() instanceof ChatScreen)) return false; - if (currentSnapshot == null || !currentSnapshot.hasSession()) return false; - if (button != 0) return false; - - boolean consumed = false; - try { - if (currentSnapshot.supportsShuffle() && hit(mx, my, shuffleX, shuffleY, buttonSize, buttonSize)) { - mediaService.toggleShuffle(); - consumed = true; - } else if (hit(mx, my, playX, playY, buttonSize, buttonSize)) { - mediaService.playPause(); - consumed = true; - } else if (hit(mx, my, prevX, prevY, buttonSize, buttonSize)) { - mediaService.previous(); - consumed = true; - } else if (hit(mx, my, nextX, nextY, buttonSize, buttonSize)) { - mediaService.next(); - consumed = true; - } else if (currentSnapshot.supportsRepeat() && hit(mx, my, repeatX, repeatY, buttonSize, buttonSize)) { - mediaService.cycleRepeatMode(); - consumed = true; - } - } catch (Throwable ignored) { - } - return consumed; - } - - @Override - public void renderEngine(Renderer2D renderer, - TextRenderer textRenderer, - GuiGraphicsExtractor ctx, - float tickDelta, - int screenW, - int screenH) { - Minecraft mc = Minecraft.getInstance(); - if (mc == null) return; - boolean chatOpen = ClientScreen.current() instanceof ChatScreen; - boolean preview = DraggableHudElementRegistry.isForceVisible(); - - MediaSessionService.Snapshot snapshot = mediaService.snapshot(); - currentSnapshot = snapshot; - MediaInfo media = snapshot.media(); - boolean hasSession = snapshot.hasSession(); - boolean mediaAvailable = snapshot.mediaAvailable(); - boolean shouldShow = hasSession || chatOpen || preview; - - float target = shouldShow ? 1.0f : 0.0f; - anim = lerp(anim, target, 0.25f); - if (!shouldShow && anim <= 0.02f) { - width = 0f; - height = 0f; - updateButtons(); - return; - } - - TextRenderer titleRenderer = Fonts.renderer("Inter", FontInfo.Type.Bold, textRenderer); - TextRenderer artistRenderer = Fonts.renderer("InterMedium", FontInfo.Type.Regular, textRenderer); - TextRenderer metaRenderer = Fonts.renderer("Inter", FontInfo.Type.Regular, textRenderer); - TextRenderer iconRenderer = getMediaIcons(textRenderer); - - float mediaScale = scale.get().floatValue(); - float baseScale = HudScale.scale(screenW, screenH) * mediaScale; - float fontScale = (hud.getFontSize() / 18f) * mediaScale; - float padding = BASE_PADDING * baseScale; - float gap = BASE_GAP * baseScale; - float artBase = BASE_ART_SIZE * baseScale; - float artBoost = BASE_ART_BOOST * baseScale; - float artOverhang = BASE_ART_OVERHANG * baseScale; - float radius = BASE_RADIUS * baseScale; - float barH = BASE_BAR_H * baseScale; - float textGap = BASE_TEXT_GAP * baseScale; - float barGap = BASE_BAR_GAP * baseScale; - float bottomGap = BASE_BOTTOM_GAP * baseScale; - float buttonGap = BASE_BUTTON_GAP * baseScale; - float contentW = BASE_CONTENT_W * baseScale; - - float titleScale = 0.95f * fontScale; - float artistScale = 0.78f * fontScale; - float metaScale = 0.70f * fontScale; - float iconScale = 0.9f * fontScale; - - String title = hasSession ? safe(media.title()) : ""; - String artist = hasSession ? safe(media.artist()) : ""; - String owner = hasSession && snapshot.session() != null ? safe(snapshot.session().getOwner()) : ""; - long predictedPos = hasSession ? snapshot.predictedPositionSeconds() : 0L; - long predictedDur = hasSession ? snapshot.durationSeconds() : 0L; - String timeText = hasSession ? formatTime(predictedPos) : ""; - - metaRenderer.begin(metaScale, false, false); - float timeW = hasSession ? (float) metaRenderer.getWidth(timeText, false) : 0f; - metaRenderer.end(); - - final boolean showShuffle = hasSession && snapshot.supportsShuffle(); - final boolean showRepeat = hasSession && snapshot.supportsRepeat(); - final int activeControls = 3 + (showShuffle ? 1 : 0) + (showRepeat ? 1 : 0); - final float btnSizeBase = BASE_BUTTON_SIZE * baseScale; - - float maxOwnerW = Math.max(0f, contentW - timeW - buttonGap); - String ownerText = hasSession ? trimText(metaRenderer, owner, maxOwnerW, metaScale) : ""; - - String titleText = hasSession ? title : ""; - String artistText = hasSession ? trimText(artistRenderer, artist, contentW, artistScale) : ""; - - titleRenderer.begin(titleScale, false, false); - float titleW = hasSession ? (float) titleRenderer.getWidth(titleText, false) : 0f; - float titleH = (float) titleRenderer.getHeight(false); - titleRenderer.end(); - - artistRenderer.begin(artistScale, false, false); - if (hasSession) { - artistRenderer.getWidth(artistText, false); - } - float artistH = (float) artistRenderer.getHeight(false); - artistRenderer.end(); - - metaRenderer.begin(metaScale, false, false); - float metaH = (float) metaRenderer.getHeight(false); - float ownerW = hasSession ? (float) metaRenderer.getWidth(ownerText, false) : 0f; - metaRenderer.end(); - - float contentH = titleH + textGap + artistH + barGap + barH + bottomGap + metaH; - float artSize = Math.max(artBase, contentH + artBoost); - float leftPad = padding + artOverhang; - - float bodyH = Math.max(contentH, artSize); - - float boxW = leftPad + padding + artSize + gap + contentW; - float boxH = padding * 2f + bodyH; - - width = boxW; - height = boxH; - - float scale = Math.max(0f, Math.min(1f, anim)); - float drawX = x + (boxW - boxW * scale) * 0.5f; - float drawY = y + (boxH - boxH * scale) * 0.5f; - - boolean useSyncTheme = syncTheme.get(); - boolean blurEnabled = isBlurEffect(); - boolean glassEnabled = isGlassEffect(); - int baseRgb = glassEnabled ? HudRenderUtil.glassBackgroundRgb() - : (useSyncTheme ? (theme().windowBg() & 0x00FFFFFF) : (bg.getArgb() & 0x00FFFFFF)); - int bgAlpha = getBgAlpha(); - int bg = (bgAlpha << 24) | baseRgb; - int accent = useSyncTheme ? theme().accent() : (this.accent.getArgb() | 0xFF000000); - int accentSoft = useSyncTheme - ? theme().accentSoft() - : HudRenderUtil.mixColor(accent, bg, 0.55f); - int stroke = HudRenderUtil.scaleAlpha(accent, 0.75f); - int strokeSoft = HudRenderUtil.scaleAlpha(accentSoft, 0.45f); - int borderColor = HudRenderUtil.mixColor(stroke, strokeSoft, 0.5f); - uiAccent = accent; - uiAccentSoft = accentSoft; - uiText = useSyncTheme ? theme().textPrimary() : (text.getArgb() | 0xFF000000); - uiTextMuted = useSyncTheme ? theme().textMuted() : HudRenderUtil.mixColor(uiText, bg, 0.6f); - - if (glassEnabled) { - drawGlass(drawX, drawY, boxW * scale, boxH * scale, radius * scale, baseScale * scale, uiAccentSoft); - } else if (blurEnabled) { - drawBlur(drawX, drawY, boxW * scale, boxH * scale, radius * scale, bg); - } - - int bgScaled = glassEnabled - ? HudRenderUtil.glassPanelBackground(scale) - : HudRenderUtil.scaleAlpha(bg, scale); - boolean useGradient = !glassEnabled && useSyncTheme && gradient.get(); - HudRenderUtil.drawHudBackground(renderer, drawX, drawY, boxW * scale, boxH * scale, radius * scale, 1.0f, bgScaled, useGradient); - if (!glassEnabled) { - renderer.roundedRectStroke(drawX, drawY, boxW * scale, boxH * scale, radius * scale, 1.0f, 0.9f * baseScale * scale, - HudRenderUtil.scaleAlpha(borderColor, scale)); - } - - float contentY = padding + (bodyH - contentH) * 0.5f; - - if (!hasSession) { - String statusKey = mediaAvailable - ? "hud.media_player.available" - : "hud.media_player.unavailable"; - String status = I18n.get(statusKey); - - metaRenderer.begin(metaScale * scale, false, false); - float statusW = (float) metaRenderer.getWidth(status, false); - float statusH = (float) metaRenderer.getHeight(false); - metaRenderer.end(); - - float sx = drawX + (boxW * scale - statusW) * 0.5f; - float sy = drawY + (boxH * scale - statusH) * 0.5f; - - metaRenderer.begin(metaScale * scale, false, false); - metaRenderer.render(status, sx, sy, new RenderColor(HudRenderUtil.scaleAlpha(uiTextMuted, scale)), false); - metaRenderer.end(); - updateButtons(); - return; - } - - float artX = leftPad - (2f * baseScale); - float artY = padding + (bodyH - artSize) * 0.5f; - float textX = leftPad + artSize + gap; - float artistY = contentY + titleH + textGap; - float barY = artistY + artistH + barGap; - float bottomY = barY + barH + bottomGap; - - float drawArtX = drawX + artX * scale; - float drawArtY = drawY + artY * scale; - float drawArtSize = artSize * scale; - float artRadius = Math.min(radius * 1.2f, artSize * 0.22f) * scale; - - Identifier artworkTexture = snapshot.artworkTexture(); - if (artworkTexture != null) { - Renderer2D.TEXTURE.roundedTexRect(drawArtX, drawArtY, drawArtSize, drawArtSize, artRadius, - 1.0f, HudRenderUtil.scaleAlpha(0xFFFFFFFF, scale), artworkTexture); - } else { - renderer.roundedRect(drawArtX, drawArtY, drawArtSize, drawArtSize, artRadius, - 1.0f, HudRenderUtil.scaleAlpha(0x44111111, scale)); - renderer.roundedRectStroke(drawArtX, drawArtY, drawArtSize, drawArtSize, artRadius, - 1.0f, 0.9f * baseScale * scale, HudRenderUtil.scaleAlpha(uiAccentSoft, scale * 0.9f)); - } - - float titleRenderX = drawX + textX * scale; - float titleRenderY = drawY + contentY * scale; - float titleRenderW = contentW * scale; - float titleRenderH = titleH * scale; - boolean titleScroll = titleW > contentW * 1.01f; - - if (titleScroll) { - drawScrollingTitle(titleRenderer, titleText, titleRenderX, titleRenderY, titleW * scale, titleRenderW, - titleScale * scale, baseScale, scale); - } else { - titleRenderer.begin(titleScale * scale, false, false); - titleRenderer.render(titleText, titleRenderX, titleRenderY, new RenderColor(HudRenderUtil.scaleAlpha(uiText, scale)), false); - titleRenderer.end(); - } - - artistRenderer.begin(artistScale * scale, false, false); - artistRenderer.render(artistText, drawX + textX * scale, drawY + artistY * scale, - new RenderColor(HudRenderUtil.scaleAlpha(uiTextMuted, scale)), false); - artistRenderer.end(); - - float barX = drawX + textX * scale; - float barW = contentW * scale; - float barRadius = Math.max(1f, barH * 0.5f) * scale; - int barBg = HudRenderUtil.scaleAlpha(0x55000000, scale); - renderer.roundedRect(barX, drawY + barY * scale, barW, barH * scale, barRadius, 1.0f, barBg); - - float progress = predictedDur > 0 ? Math.min(1.0f, Math.max(0.0f, - (float) predictedPos / (float) predictedDur)) : 0f; - int barFill = HudRenderUtil.scaleAlpha(uiAccent, scale); - renderer.roundedRect(barX, drawY + barY * scale, barW * progress, barH * scale, barRadius, 1.0f, barFill); - - metaRenderer.begin(metaScale * scale, false, false); - metaRenderer.render(timeText, drawX + textX * scale, drawY + bottomY * scale, - new RenderColor(HudRenderUtil.scaleAlpha(uiTextMuted, scale)), false); - - float ownerX = drawX + (textX + contentW - ownerW) * scale; - metaRenderer.render(ownerText, ownerX, drawY + bottomY * scale, - new RenderColor(HudRenderUtil.scaleAlpha(uiTextMuted, scale)), false); - metaRenderer.end(); - - buttonSize = btnSizeBase * scale; - - float controlsReservedW = btnSizeBase * activeControls + buttonGap * Math.max(0, activeControls - 1); - float controlsX = textX + (contentW - controlsReservedW) * 0.5f; - float controlsY = bottomY + (metaH - btnSizeBase) * 0.5f; - float cursorX = controlsX; - - if (showShuffle) { - shuffleX = drawX + cursorX * scale; - shuffleY = drawY + controlsY * scale; - cursorX += btnSizeBase + buttonGap; - } else { - shuffleX = shuffleY = 0f; - } - - prevX = drawX + cursorX * scale; - prevY = drawY + controlsY * scale; - cursorX += btnSizeBase + buttonGap; - - playX = drawX + cursorX * scale; - playY = prevY; - cursorX += btnSizeBase + buttonGap; - - nextX = drawX + cursorX * scale; - nextY = prevY; - cursorX += btnSizeBase + buttonGap; - - if (showRepeat) { - repeatX = drawX + cursorX * scale; - repeatY = prevY; - } else { - repeatX = repeatY = 0f; - } - - float controlIconScale = iconScale * 1.05f; - - updateHover(chatOpen, showShuffle, showRepeat); - if (showShuffle) { - drawControl(renderer, iconRenderer, iconString(ICON_SHUFFLE), - shuffleX, shuffleY, buttonSize, shuffleHover, scale, controlIconScale, shuffleColor(snapshot, scale)); - } - - drawControl(renderer, iconRenderer, iconString(ICON_PREV), - prevX, prevY, buttonSize, prevHover, scale, controlIconScale, HudRenderUtil.scaleAlpha(uiText, scale)); - - drawControl(renderer, iconRenderer, - iconString(snapshot.isPlaying() ? ICON_PAUSE : ICON_PLAY), - playX, playY, buttonSize, playHover, scale, controlIconScale, HudRenderUtil.scaleAlpha(uiText, scale)); - - drawControl(renderer, iconRenderer, iconString(ICON_NEXT), - nextX, nextY, buttonSize, nextHover, scale, controlIconScale, HudRenderUtil.scaleAlpha(uiText, scale)); - - if (showRepeat) { - drawRepeatControl(renderer, snapshot, repeatX, repeatY, buttonSize, repeatHover, scale); - } - } - - private void drawControl(Renderer2D renderer, - TextRenderer iconRenderer, - String icon, - float x, - float y, - float size, - float hover, - float alpha, - float iconScale, - int iconColor) { - if (size <= 0f) return; - float radius = size * 0.5f; - float hoverEase = clamp01(hover); - int bg = HudRenderUtil.scaleAlpha(uiAccentSoft, hoverEase * 0.45f * alpha); - if (hoverEase > 0.01f) { - renderer.roundedRect(x, y, size, size, radius, 1.0f, bg); - } - - iconRenderer.begin(iconScale * alpha, false, false); - float iw = (float) iconRenderer.getWidth(icon, false); - float ih = (float) iconRenderer.getHeight(false); - float ix = x + (size - iw) * 0.5f; - float iy = y + (size - ih) * 0.5f; - iconRenderer.render(icon, ix, iy, new RenderColor(iconColor), false); - iconRenderer.end(); - } - - private void drawRepeatControl(Renderer2D renderer, - MediaSessionService.Snapshot snapshot, - float x, - float y, - float size, - float hover, - float alpha) { - if (size <= 0f) return; - float radius = size * 0.5f; - float hoverEase = clamp01(hover); - int bg = HudRenderUtil.scaleAlpha(uiAccentSoft, hoverEase * 0.45f * alpha); - if (hoverEase > 0.01f) { - renderer.roundedRect(x, y, size, size, radius, 1.0f, bg); - } - - String icon = repeatSvg(snapshot); - int color = repeatColor(snapshot, alpha); - float iconSize = size * 0.78f; - float ix = x + (size - iconSize) * 0.5f; - float iy = y + (size - iconSize) * 0.5f; - renderer.svg(icon, ix, iy, iconSize, iconSize, SvgRenderOptions.overrideColor(color)); - } - - private void updateHover(boolean chatOpen, boolean showShuffle, boolean showRepeat) { - Minecraft mc = Minecraft.getInstance(); - if (!chatOpen) { - playHover = lerp(playHover, 0f, 0.2f); - shuffleHover = lerp(shuffleHover, 0f, 0.2f); - prevHover = lerp(prevHover, 0f, 0.2f); - nextHover = lerp(nextHover, 0f, 0.2f); - repeatHover = lerp(repeatHover, 0f, 0.2f); - return; - } - int fbw = mc.getWindow().getWidth(); - int fbh = mc.getWindow().getHeight(); - float uiScale = HudScale.scale(fbw, fbh); - float mx = HudScale.toVirtual((float) mc.mouseHandler.xpos(), uiScale); - float my = HudScale.toVirtual((float) mc.mouseHandler.ypos(), uiScale); - shuffleHover = lerp(shuffleHover, showShuffle && hit(mx, my, shuffleX, shuffleY, buttonSize, buttonSize) ? 1f : 0f, 0.25f); - playHover = lerp(playHover, hit(mx, my, playX, playY, buttonSize, buttonSize) ? 1f : 0f, 0.25f); - prevHover = lerp(prevHover, hit(mx, my, prevX, prevY, buttonSize, buttonSize) ? 1f : 0f, 0.25f); - nextHover = lerp(nextHover, hit(mx, my, nextX, nextY, buttonSize, buttonSize) ? 1f : 0f, 0.25f); - repeatHover = lerp(repeatHover, showRepeat && hit(mx, my, repeatX, repeatY, buttonSize, buttonSize) ? 1f : 0f, 0.25f); - } - - private void updateButtons() { - shuffleX = (float) 0.0; - shuffleY = (float) 0.0; - playX = (float) 0.0; - playY = (float) 0.0; - prevX = (float) 0.0; - prevY = (float) 0.0; - nextX = (float) 0.0; - nextY = (float) 0.0; - repeatX = (float) 0.0; - repeatY = (float) 0.0; - buttonSize = (float) 0.0; - playHover = 0f; - shuffleHover = 0f; - prevHover = 0f; - nextHover = 0f; - repeatHover = 0f; - } - - private int shuffleColor(MediaSessionService.Snapshot snapshot, float alpha) { - if (snapshot == null || !snapshot.supportsShuffle()) { - return HudRenderUtil.scaleAlpha(0xFF000000, 0.36f * alpha); - } - return snapshot.isShuffleActive() - ? HudRenderUtil.scaleAlpha(0xFFFFFFFF, alpha) - : HudRenderUtil.scaleAlpha(0xFFB7BEC8, 0.48f * alpha); - } - - private int repeatColor(MediaSessionService.Snapshot snapshot, float alpha) { - if (snapshot == null || !snapshot.supportsRepeat()) { - return HudRenderUtil.scaleAlpha(0xFF000000, 0.36f * alpha); - } - RepeatMode mode = snapshot.repeatMode(); - return mode == RepeatMode.ALL || mode == RepeatMode.ONE - ? HudRenderUtil.scaleAlpha(0xFFFFFFFF, alpha) - : HudRenderUtil.scaleAlpha(0xFFB7BEC8, 0.48f * alpha); - } - - private String repeatSvg(MediaSessionService.Snapshot snapshot) { - if (snapshot == null || !snapshot.supportsRepeat()) { - return "repeat-off"; - } - return snapshot.repeatMode() == RepeatMode.ONE ? "repeat-1" : "repeat"; - } - - private String formatTime(long seconds) { - long sec = Math.max(0L, seconds); - long m = sec / 60L; - long s = sec % 60L; - return String.format("%d:%02d", m, s); - } - - private String safe(String value) { - if (value == null) return ""; - String v = value.trim(); - return v.isEmpty() ? "" : v; - } - - private float lerp(float a, float b, float t) { - return a + (b - a) * t; - } - - private boolean hit(float mx, float my, float x, float y, float w, float h) { - return mx >= x && mx <= x + w && my >= y && my <= y + h; - } - - private float clamp01(float v) { - return Math.max(0f, Math.min(1f, v)); - } - - private void drawScrollingTitle(TextRenderer renderer, - String text, - float x, - float y, - float fullWidth, - float viewWidth, - float scale, - float baseScale, - float alpha) { - float speed = BASE_TITLE_SCROLL_SPEED * baseScale; - float gap = BASE_TITLE_SCROLL_GAP * baseScale; - if (speed <= 0f) return; - float cycleDistance = fullWidth + gap; - float cycleTime = TITLE_SCROLL_PAUSE_SEC + (cycleDistance / speed); - if (cycleTime <= 0f) return; - - float now = Util.getMillis() / 1000f; - float t = now % cycleTime; - float offset = 0f; - if (t > TITLE_SCROLL_PAUSE_SEC) { - offset = -(t - TITLE_SCROLL_PAUSE_SEC) * speed; - } - - int color = HudRenderUtil.scaleAlpha(uiText, alpha); - RenderColor renderColor = new RenderColor(color); - float fade = Math.min(viewWidth * 0.25f, BASE_TITLE_FADE * baseScale); - renderer.begin(scale, false, false); - try { - renderer.renderHorizontalFadeClipped(text, x + offset, y, renderColor, x, x + viewWidth, fade, fade, false); - if (cycleDistance > viewWidth * 0.5f) { - renderer.renderHorizontalFadeClipped(text, x + offset + cycleDistance, y, renderColor, - x, x + viewWidth, fade, fade, false); - } - } finally { - renderer.end(); - } - } - - private float getTextHeight(TextRenderer renderer, float scale) { - renderer.begin(scale, false, false); - try { - return (float) renderer.getHeight(false); - } finally { - renderer.end(); - } - } - - private String trimText(TextRenderer tr, String text, float maxWidth, float scale) { - if (text == null || text.isEmpty()) return ""; - tr.begin(scale, false, false); - try { - if (tr.getWidth(text, false) <= maxWidth) return text; - String value = text; - while (!value.isEmpty()) { - String candidate = value + "..."; - if (tr.getWidth(candidate, false) <= maxWidth) return candidate; - value = value.substring(0, value.length() - 1); - } - return ""; - } finally { - tr.end(); - } - } - - private void drawBlur(float x, float y, float w, float h, float radius, int tintRgb) { - float quality = hud.getBlurRadius(); - float brightness = 1.0f; - float alpha = blurAlpha.get() / 255f; - Renderer2D.COLOR.blurRect(x, y, w, h, radius, quality, brightness, alpha, 0xFFFFFF); - } - - private void drawGlass(float x, float y, float w, float h, float radius, float scale, int accentSoft) { - float alpha = blurAlpha.get() / 255f; - HudRenderUtil.drawLiquidGlass(x, y, w, h, radius, scale, true, alpha); - } - - private boolean hasEffect() { - return !EFFECT_NONE.equals(bgEffect.get()); - } - - private boolean isBlurEffect() { - return EFFECT_BLUR.equals(bgEffect.get()); - } - - private boolean isGlassEffect() { - return EFFECT_GLASS.equals(bgEffect.get()); - } - - private int getBgAlpha() { - if (syncTheme.get()) { - return bgAlpha.get(); - } - return (bg.getArgb() >>> 24) & 0xFF; - } - @Override - public void collectRenderPrewarm(RenderPrewarmCollectEvent event) { - super.collectRenderPrewarm(event); - event.font("MediaPlayer", FontInfo.Type.Regular) - .font("Inter", FontInfo.Type.Bold) - .font("InterMedium", FontInfo.Type.Regular) - .svg("repeat") - .svg("repeat-1") - .svg("repeat-off"); - } - -} - diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/Coordinates.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/Coordinates.java deleted file mode 100644 index fe0e51f..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/Coordinates.java +++ /dev/null @@ -1,337 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.features.gui.hud.draggable.impl.metrics; - -import dev.loki.lovisual.config.values.color.RGBAColorValue; -import dev.loki.lovisual.config.values.color.RGBColorValue; -import dev.loki.lovisual.config.values.mode.EnumValue; -import dev.loki.lovisual.config.values.mode.ModeValue; -import dev.loki.lovisual.config.values.primitive.NumberValue; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.resources.Identifier; -import net.minecraft.util.Mth; -import net.minecraft.util.Util; -import net.minecraft.world.level.Level; -import dev.loki.lovisual.features.gui.hud.registry.HudElementInfo; -import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; -import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; -import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; -import dev.loki.lovisual.features.gui.hud.script.CompactHudStatModel; -import dev.loki.lovisual.features.gui.hud.script.ScriptableHudStatWidget; -import dev.loki.lovisual.features.gui.hud.script.ScriptedCompactHudStatRenderer; -import dev.loki.lovisual.render.engine.animation.AnimationUtility; -import dev.loki.lovisual.render.engine.math.HudScale; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.text.font.info.FontInfo; -import dev.loki.lovisual.render.engine.text.font.Fonts; -import dev.loki.lovisual.render.engine.text.render.TextRenderer; - -import static dev.loki.lovisual.features.theme.Theme.theme; - -//todo Description -@HudElementInfo( - id = "xyz", - displayName = "XYZ", - enabledByDefault = true, - order = 100 -) -public final class Coordinates extends DraggableHudElement implements ScriptableHudStatWidget { - - private static final Identifier COORDS_ICON = Identifier.fromNamespaceAndPath("lovisual", "textures/hud/elements/coords.png"); - private static final float SCALE_MULT = 0.68f; - private static final float BOX_HEIGHT = 20f; - private static final float BOX_RADIUS = 5f; - private static final float BOX_SOFTNESS = 1.0f; - private static final float STROKE_WIDTH = 0.55f; - private static final float ICON_SIZE = 11f; - private static final float ICON_X = 5f; - private static final float DIVIDER_X = 21f; - private static final float DIVIDER_Y = 4f; - private static final float DIVIDER_WIDTH = 0.6f; - private static final float TEXT_LEFT_X = 26f; - private static final float SEGMENT_GAP = 8f; - private static final float LABEL_VALUE_GAP = 2f; - private static final float RIGHT_PAD = 5f; - private static final String COLOR_THEME = "Theme"; - private static final String COLOR_CUSTOM = "Custom"; - private static final String EFFECT_NONE = "None"; - private static final String EFFECT_BLUR = "Blur"; - private static final String EFFECT_GLASS = "Glass"; - - private final HudGlobalConfig hud = HudGlobalConfig.get(); - private final EnumValue netherMode = - enumSetting("xyz_nether", NetherMode.ON, NetherMode.OFF, NetherMode.ON, NetherMode.ONLY_NETHER); - private final NumberValue scale = num("xyz_scale", 2.37, 0.5, 5.0); - private final ModeValue colorMode = mode("xyz_color_mode", "color_mode", "Theme", new String[]{COLOR_THEME, COLOR_CUSTOM}); - private final RGBColorValue iconColor = visibleWhen(colorNoAlpha("xyz_icon_color", "#FFFFFF"), this::isCustomMode); - private final RGBColorValue labelColor = visibleWhen(colorNoAlpha("xyz_label_color", "#9B9B9B"), this::isCustomMode); - private final RGBColorValue valueColor = visibleWhen(colorNoAlpha("xyz_value_color", "#FFFFFF"), this::isCustomMode); - private final RGBColorValue extraColor = visibleWhen(colorNoAlpha("xyz_extra_color", "#BEBEBE"), this::isCustomMode); - private final ModeValue bgEffect = mode("xyz_bg_effect", "bg_effect", "Blur", new String[]{EFFECT_NONE, EFFECT_BLUR, EFFECT_GLASS}); - private final RGBAColorValue bg = visibleWhen(color("xyz_bg", "#F7343434"), () -> isCustomMode() && !isGlassEffect()); - private final RGBAColorValue bg2 = visibleWhen(color("xyz_bg_secondary", "#F7161616"), () -> isCustomMode() && !isGlassEffect()); - private final RGBColorValue stroke = visibleWhen(colorNoAlpha("xyz_stroke", "#5A5A5A"), () -> isCustomMode() && !isGlassEffect()); - private final NumberValue bgAlpha = visibleWhen(num("xyz_bg_alpha", 184, 0, 255), () -> isThemeMode() && !isGlassEffect()); - private final NumberValue blurAlpha = visibleWhen(num("xyz_blur_alpha", 255, 0, 255), this::hasEffect); - private final EnumValue labelEffect = - enumSetting("xyz_label_effect", HudTextEffects.Effect.FLOW, - HudTextEffects.Effect.NONE, HudTextEffects.Effect.MIX, HudTextEffects.Effect.FLOW, - HudTextEffects.Effect.PULSE, HudTextEffects.Effect.STRIPE); - private final NumberValue labelEffectSpeed = - visibleWhen(num("xyz_label_effect_speed", 18, 1, 60), - () -> labelEffect.get() != HudTextEffects.Effect.NONE); - private final CompactHudStatModel scriptModel = new CompactHudStatModel("xyz"); - private int uiBgPrimary; - private int uiBgSecondary; - private int uiStroke; - private int uiIconColor; - private int uiLabelColor; - private int uiValueColor; - private int uiExtraColor; - private float displayXValueWidth = -1f; - private float displayYValueWidth = -1f; - private float displayZValueWidth = -1f; - private float displayExtraWidth = -1f; - - private static float smoothWidth(float current, float target) { - if (current < 0.0f) return target; - if (target >= current) return target; - float next = AnimationUtility.approach(current, target, AnimationUtility.deltaTime(), 12.0f); - return AnimationUtility.snap(next, target, 0.25f); - } - - @Override - public void applyDefaultPosition(int screenW, int screenH) { - this.x = 16f; - this.y = screenH - 92f; - } - - @Override - public boolean usesEngineRenderer() { - return true; - } - - @Override - public void renderEngine(Renderer2D renderer, - TextRenderer textRenderer, - GuiGraphicsExtractor ctx, - float tickDelta, - int screenW, - int screenH) { - boolean preview = DraggableHudElementRegistry.isForceVisible(); - Minecraft mc = Minecraft.getInstance(); - if (mc == null || (mc.player == null && !preview)) { - width = 0f; - height = 0f; - scriptModel.setVisible(false); - return; - } - if (!preview && !isEnabled()) { - width = 0f; - height = 0f; - scriptModel.setVisible(false); - return; - } - - boolean inNether = mc.level != null && mc.level.dimension() == Level.NETHER; - double rawX = mc.player != null ? mc.player.getX() : 0.0; - double rawY = mc.player != null ? mc.player.getY() : 0.0; - double rawZ = mc.player != null ? mc.player.getZ() : 0.0; - int px = Mth.floor(rawX); - int py = Mth.floor(rawY); - int pz = Mth.floor(rawZ); - boolean showNether = shouldShowNether(inNether); - int hx = showNether ? (int) (rawX * (inNether ? 8.0 : 0.125)) : 0; - int hz = showNether ? (int) (rawZ * (inNether ? 8.0 : 0.125)) : 0; - updatePalette(); - - String xValue = Integer.toString(px); - String yValue = Integer.toString(py); - String zValue = Integer.toString(pz); - String extra = showNether ? "[" + hx + " " + hz + "]" : ""; - - TextRenderer fallback = textRenderer != null ? textRenderer : TextRenderer.get(); - TextRenderer tr = Fonts.renderer("Onest", FontInfo.Type.Regular, fallback); - - float drawScale = HudScale.scale(screenW, screenH) - * (hud.getFontSize() / 18f) - * SCALE_MULT - * this.scale.get().floatValue(); - - tr.begin(drawScale, true, false); - float labelXW = (float) tr.getWidth("x", false); - float labelYW = (float) tr.getWidth("y", false); - float labelZW = (float) tr.getWidth("z", false); - float valueXW = (float) tr.getWidth(xValue, false); - float valueYW = (float) tr.getWidth(yValue, false); - float valueZW = (float) tr.getWidth(zValue, false); - float extraW = showNether ? (float) tr.getWidth(extra, false) : 0f; - float textH = (float) tr.getHeight(false); - tr.end(); - - float boxH = BOX_HEIGHT * drawScale; - float iconSize = ICON_SIZE * drawScale; - float cursorX = x + TEXT_LEFT_X * drawScale; - float gap = SEGMENT_GAP * drawScale; - float pairGap = LABEL_VALUE_GAP * drawScale; - displayXValueWidth = smoothWidth(displayXValueWidth, valueXW); - displayYValueWidth = smoothWidth(displayYValueWidth, valueYW); - displayZValueWidth = smoothWidth(displayZValueWidth, valueZW); - displayExtraWidth = showNether ? smoothWidth(displayExtraWidth, extraW) : 0.0f; - float boxW = (cursorX - x) - + labelXW + pairGap + displayXValueWidth - + gap - + labelYW + pairGap + displayYValueWidth - + gap - + labelZW + pairGap + displayZValueWidth - + (showNether ? gap + displayExtraWidth : 0f) - + RIGHT_PAD * drawScale; - - width = boxW; - height = boxH; - - float baseX = x; - float baseY = y; - float iconX = baseX + ICON_X * drawScale; - float iconY = baseY + (boxH - iconSize) * 0.5f; - float dividerX = baseX + DIVIDER_X * drawScale; - float dividerY = baseY + DIVIDER_Y * drawScale; - float dividerW = Math.max(0.5f, DIVIDER_WIDTH * drawScale); - float dividerH = Math.max(0.5f, boxH - 2f * DIVIDER_Y * drawScale); - float textY = baseY + (boxH - textH) * 0.5f; - - int iconColor = uiIconColor; - boolean useLabelEffect = labelEffect.get() != HudTextEffects.Effect.NONE; - float time = useLabelEffect ? (float) (Util.getMillis() / 1000.0) : 0.0f; - if (useLabelEffect) { - iconColor = HudTextEffects.animatedColor(iconColor, labelEffect.get(), - labelEffectSpeed.get(), time, 0.0f); - } - - float radius = BOX_RADIUS * drawScale; - scriptModel.setVisible(true); - scriptModel.setRoot(baseX, baseY, boxW, boxH, radius, drawScale); - scriptModel.background().set(bgEffect.get(), isThemeMode(), blurAlpha.get() / 255.0f, - uiBgPrimary, uiBgSecondary, uiStroke, Math.max(0.5f, STROKE_WIDTH * drawScale), BOX_SOFTNESS); - scriptModel.icon().texture(COORDS_ICON.toString(), iconX - baseX, iconY - baseY, iconSize, iconSize, iconColor); - scriptModel.divider().set(dividerX - baseX, dividerY - baseY, dividerW, dividerH, - HudRenderUtil.setAlpha(uiLabelColor, 0x52)); - scriptModel.value().set(xValue + " " + yValue + " " + zValue, "Onest", drawScale, - cursorX - baseX, textY - baseY, boxW - (cursorX - baseX), uiValueColor); - scriptModel.unit().hidden(); - if (showNether) { - scriptModel.extra().set(extra, "OnestMedium", drawScale, - boxW - RIGHT_PAD * drawScale - displayExtraWidth, textY - baseY, displayExtraWidth, uiExtraColor); - } else { - scriptModel.extra().hidden(); - } - scriptModel.animation().setLabelEffect(labelEffect.get().name(), labelEffectSpeed.get(), time) - .setDigitAnimation(false, "", 1.0f, 0.0f); - scriptModel.data("x", px) - .data("y", py) - .data("z", pz) - .data("xText", xValue) - .data("yText", yValue) - .data("zText", zValue) - .data("showNether", showNether) - .data("netherX", hx) - .data("netherZ", hz) - .data("netherText", extra) - .data("labelXW", labelXW) - .data("labelYW", labelYW) - .data("labelZW", labelZW) - .data("valueXW", displayXValueWidth) - .data("valueYW", displayYValueWidth) - .data("valueZW", displayZValueWidth) - .data("xyzExtraW", displayExtraWidth) - .data("labelColor", CompactHudStatModel.colorString(uiLabelColor)) - .data("valueColor", CompactHudStatModel.colorString(uiValueColor)); - ScriptedCompactHudStatRenderer.INSTANCE.render(scriptModel, renderer, fallback, ctx, tickDelta); - } - - @Override - public boolean supportsWidgetAnchoring() { - return true; - } - - @Override - public CompactHudStatModel compactStatModel() { - return scriptModel; - } - - private boolean shouldShowNether(boolean inNether) { - NetherMode mode = netherMode.get(); - if (mode == NetherMode.ON) return true; - if (mode == NetherMode.ONLY_NETHER) return !inNether; - return false; - } - - private void updatePalette() { - if (isThemeMode()) { - int panelAlpha = bgAlpha.get(); - uiBgPrimary = HudRenderUtil.setAlpha(theme().windowBg(), panelAlpha); - uiBgSecondary = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme().surface(), theme().windowHeader(), 0.35f), - panelAlpha - ); - uiStroke = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme().windowStroke(), theme().strokeSoft(), 0.4f), - Math.min(panelAlpha, 190) - ); - uiIconColor = theme().textPrimary(); - uiLabelColor = theme().textMuted(); - uiValueColor = theme().textPrimary(); - uiExtraColor = HudRenderUtil.mixColor(theme().textMuted(), theme().textPrimary(), 0.18f); - return; - } - - uiBgPrimary = bg.getArgb(); - uiBgSecondary = bg2.getArgb(); - uiStroke = stroke.getArgb(); - uiIconColor = iconColor.getArgb(); - uiLabelColor = labelColor.getArgb(); - uiValueColor = valueColor.getArgb(); - uiExtraColor = extraColor.getArgb(); - } - - private boolean isThemeMode() { - return COLOR_THEME.equals(colorMode.get()); - } - - private boolean isCustomMode() { - return COLOR_CUSTOM.equals(colorMode.get()); - } - - private boolean isGlassEffect() { - return EFFECT_GLASS.equals(bgEffect.get()); - } - - private boolean hasEffect() { - return !EFFECT_NONE.equals(bgEffect.get()); - } - - private enum NetherMode implements EnumValue.IdProvider { - OFF("Off"), - ON("On"), - ONLY_NETHER("OnlyNether"); - - private final String id; - - NetherMode(String id) { - this.id = id; - } - - @Override - public String id() { - return id; - } - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/GameTime.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/GameTime.java deleted file mode 100644 index ab5da69..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/GameTime.java +++ /dev/null @@ -1,314 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.features.gui.hud.draggable.impl.metrics; - -import dev.loki.lovisual.config.values.color.RGBAColorValue; -import dev.loki.lovisual.config.values.color.RGBColorValue; -import dev.loki.lovisual.config.values.mode.EnumValue; -import dev.loki.lovisual.config.values.mode.ModeValue; -import dev.loki.lovisual.config.values.primitive.BooleanValue; -import dev.loki.lovisual.config.values.primitive.NumberValue; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.core.BlockPos; -import net.minecraft.util.Util; -import net.minecraft.world.level.biome.Biome; -import dev.loki.lovisual.features.gui.hud.registry.HudElementInfo; -import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; -import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; -import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; -import dev.loki.lovisual.features.gui.hud.script.CompactHudStatModel; -import dev.loki.lovisual.features.gui.hud.script.ScriptableHudStatWidget; -import dev.loki.lovisual.features.gui.hud.script.ScriptedCompactHudStatRenderer; -import dev.loki.lovisual.features.module.modules.visuals.weather.WorldTweaks; -import dev.loki.lovisual.render.engine.animation.AnimationUtility; -import dev.loki.lovisual.render.engine.math.HudScale; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.text.font.info.FontInfo; -import dev.loki.lovisual.render.engine.text.font.Fonts; -import dev.loki.lovisual.render.engine.text.render.TextRenderer; - -import static dev.loki.lovisual.features.theme.Theme.theme; - -@HudElementInfo( - id = "game_time", - displayName = "Game Time", - enabledByDefault = false, - order = 180 -) -public final class GameTime extends DraggableHudElement implements ScriptableHudStatWidget { - - private static final float SCALE_MULT = 0.68f; - private static final float BOX_HEIGHT = 20f; - private static final float BOX_RADIUS = 5f; - private static final float BOX_SOFTNESS = 1.0f; - private static final float STROKE_WIDTH = 0.55f; - private static final float ICON_X = 5f; - private static final float DIVIDER_X = 21f; - private static final float DIVIDER_Y = 4f; - private static final float DIVIDER_WIDTH = 0.6f; - private static final float TEXT_LEFT_X = 26f; - private static final float RIGHT_PAD = 5f; - private static final float ICON_SCALE = 0.92f; - private static final String WIDTH_TEMPLATE = "00:00"; - private static final String COLOR_THEME = "Theme"; - private static final String COLOR_CUSTOM = "Custom"; - private static final String EFFECT_NONE = "None"; - private static final String EFFECT_BLUR = "Blur"; - private static final String EFFECT_GLASS = "Glass"; - private static final long SUNRISE_START = 23000L; - private static final long SUNRISE_END = 1000L; - private static final long SUNSET_START = 12000L; - private static final long SUNSET_END = 13000L; - private static final int ICON_SUNRISE = 0xEA0E; - private static final int ICON_SUN = 0xEA04; - private static final int ICON_SUNSET = 0xEA0F; - private static final int ICON_RAIN_DAY = 0xEA01; - private static final int ICON_RAIN_NIGHT = 0xEA09; - private static final int ICON_THUNDER_DAY = 0xEA05; - private static final int ICON_THUNDER_NIGHT = 0xEA0C; - private static final int ICON_SNOW_DAY = 0xEA03; - private static final int ICON_SNOW_NIGHT = 0xEA0B; - private static final int ICON_THUNDER_SNOW_DAY = 0xEA02; - private static final int ICON_THUNDER_SNOW_NIGHT = 0xEA0A; - - private final HudGlobalConfig hud = HudGlobalConfig.get(); - private final BooleanValue showWeather = bool("game_time_weather", "weather", true); - private final NumberValue scale = num("game_time_scale", 2.37, 0.5, 5.0); - private final ModeValue colorMode = mode("game_time_color_mode", "color_mode", "Theme", new String[]{COLOR_THEME, COLOR_CUSTOM}); - private final RGBColorValue iconColor = visibleWhen(colorNoAlpha("game_time_icon_color", "#FFFFFF"), this::isCustomMode); - private final RGBColorValue valueColor = visibleWhen(colorNoAlpha("game_time_value_color", "#FFFFFF"), this::isCustomMode); - private final ModeValue bgEffect = mode("game_time_bg_effect", "bg_effect", "None", new String[]{EFFECT_NONE, EFFECT_BLUR, EFFECT_GLASS}); - private final RGBAColorValue bg = visibleWhen(color("game_time_bg", "#EB111318"), () -> isCustomMode() && !isGlassEffect()); - private final RGBAColorValue bg2 = visibleWhen(color("game_time_bg_secondary", "#F7161616"), () -> isCustomMode() && !isGlassEffect()); - private final RGBColorValue stroke = visibleWhen(colorNoAlpha("game_time_stroke", "#5A5A5A"), () -> isCustomMode() && !isGlassEffect()); - private final NumberValue bgAlpha = visibleWhen(num("game_time_bg_alpha", 235, 0, 255), () -> isThemeMode() && !isGlassEffect()); - private final NumberValue blurAlpha = visibleWhen(num("game_time_blur_alpha", 140, 0, 255), this::hasEffect); - private final EnumValue iconEffect = - enumSetting("game_time_icon_effect", HudTextEffects.Effect.MIX, - HudTextEffects.Effect.NONE, HudTextEffects.Effect.MIX, HudTextEffects.Effect.FLOW, - HudTextEffects.Effect.PULSE, HudTextEffects.Effect.STRIPE); - private final NumberValue iconEffectSpeed = - visibleWhen(num("game_time_icon_effect_speed", 18, 1, 60), - () -> iconEffect.get() != HudTextEffects.Effect.NONE); - private final CompactHudStatModel scriptModel = new CompactHudStatModel("game_time"); - - private int uiBgPrimary; - private int uiBgSecondary; - private int uiStroke; - private int uiIconColor; - private int uiValueColor; - private float displayTimeWidth = -1f; - - private static String iconString(int codepoint) { - return new String(Character.toChars(codepoint)); - } - - private static TextRenderer getWeatherIcons() { - return Fonts.renderer("WeatherIcons", FontInfo.Type.Regular, TextRenderer.get()); - } - - private static String formatTime(long time) { - int hours = (int) ((time / 1000 + 6) % 24); - int minutes = (int) ((time % 1000) * 60 / 1000); - return String.format("%02d:%02d", hours, minutes); - } - - private static float smoothWidth(float current, float target) { - if (current < 0.0f) return target; - if (target >= current) return target; - float next = AnimationUtility.approach(current, target, AnimationUtility.deltaTime(), 12.0f); - return AnimationUtility.snap(next, target, 0.25f); - } - - @Override - public void applyDefaultPosition(int screenW, int screenH) { - this.x = 16f; - this.y = 76f; - } - - @Override - public boolean usesEngineRenderer() { - return true; - } - - @Override - public void renderEngine(Renderer2D renderer, - TextRenderer textRenderer, - GuiGraphicsExtractor ctx, - float tickDelta, - int screenW, - int screenH) { - boolean preview = DraggableHudElementRegistry.isForceVisible(); - Minecraft mc = Minecraft.getInstance(); - if ((mc == null || mc.level == null) && !preview) { - width = 0f; - height = 0f; - scriptModel.setVisible(false); - return; - } - if (!preview && !isEnabled()) { - width = 0f; - height = 0f; - scriptModel.setVisible(false); - return; - } - - long worldTime = mc != null && mc.level != null ? (WorldTweaks.getServerTimeOfDay() % 24000L) : 6000L; - String icon = iconString(resolveIcon(worldTime)); - String timeText = formatTime(worldTime); - updatePalette(); - - TextRenderer fallback = textRenderer != null ? textRenderer : TextRenderer.get(); - TextRenderer iconRenderer = getWeatherIcons(); - if (iconRenderer == null) iconRenderer = fallback; - TextRenderer valueRenderer = Fonts.renderer("Onest", FontInfo.Type.Regular, fallback); - - float drawScale = HudScale.scale(screenW, screenH) - * (hud.getFontSize() / 18f) - * SCALE_MULT - * scale.get().floatValue(); - - float iconScale = drawScale * ICON_SCALE; - iconRenderer.begin(iconScale, true, false); - float iconW = (float) iconRenderer.getWidth(icon, false); - float iconH = (float) iconRenderer.getHeight(false); - iconRenderer.end(); - - valueRenderer.begin(drawScale, true, false); - float timeW = (float) valueRenderer.getWidth(timeText, false); - float timeH = (float) valueRenderer.getHeight(false); - float reservedTimeW = (float) valueRenderer.getWidth(WIDTH_TEMPLATE, false); - valueRenderer.end(); - - displayTimeWidth = Math.max(smoothWidth(displayTimeWidth, timeW), reservedTimeW); - float boxH = BOX_HEIGHT * drawScale; - float boxW = TEXT_LEFT_X * drawScale + displayTimeWidth + RIGHT_PAD * drawScale; - width = boxW; - height = boxH; - - float baseX = x; - float baseY = y; - float iconX = baseX + ICON_X * drawScale; - float iconY = baseY + (boxH - iconH) * 0.5f; - float dividerX = baseX + DIVIDER_X * drawScale; - float dividerY = baseY + DIVIDER_Y * drawScale; - float dividerW = Math.max(0.5f, DIVIDER_WIDTH * drawScale); - float dividerH = Math.max(0.5f, boxH - 2f * DIVIDER_Y * drawScale); - float textX = baseX + TEXT_LEFT_X * drawScale; - float textY = baseY + (boxH - timeH) * 0.5f; - - int resolvedIconColor = uiIconColor; - boolean useIconEffect = iconEffect.get() != HudTextEffects.Effect.NONE; - float nowSec = useIconEffect ? (float) (Util.getMillis() / 1000.0) : 0.0f; - if (useIconEffect) { - resolvedIconColor = HudTextEffects.animatedColor(resolvedIconColor, iconEffect.get(), - iconEffectSpeed.get(), nowSec, 0.0f); - } - - float radius = BOX_RADIUS * drawScale; - scriptModel.setVisible(true); - scriptModel.setRoot(baseX, baseY, boxW, boxH, radius, drawScale); - scriptModel.background().set(bgEffect.get(), isThemeMode(), blurAlpha.get() / 255.0f, - uiBgPrimary, uiBgSecondary, uiStroke, Math.max(0.5f, STROKE_WIDTH * drawScale), BOX_SOFTNESS); - scriptModel.icon().glyph(icon, "WeatherIcons", iconX - baseX, iconY - baseY, iconW, iconH, iconScale, resolvedIconColor); - scriptModel.divider().set(dividerX - baseX, dividerY - baseY, dividerW, dividerH, - HudRenderUtil.setAlpha(uiValueColor, 0x52)); - scriptModel.value().set(timeText, "Onest", drawScale, textX - baseX, textY - baseY, displayTimeWidth, uiValueColor); - scriptModel.unit().hidden(); - scriptModel.extra().hidden(); - scriptModel.animation().setLabelEffect(iconEffect.get().name(), iconEffectSpeed.get(), nowSec) - .setDigitAnimation(false, "", 1.0f, 0.0f); - scriptModel.data("worldTime", worldTime) - .data("timeText", timeText) - .data("weatherIcon", icon) - .data("showWeather", showWeather.get()); - ScriptedCompactHudStatRenderer.INSTANCE.render(scriptModel, renderer, fallback, ctx, tickDelta); - } - - @Override - public CompactHudStatModel compactStatModel() { - return scriptModel; - } - - private int resolveIcon(long time) { - if (!showWeather.get()) return resolveClearIcon(time); - Minecraft mc = Minecraft.getInstance(); - if (mc == null || mc.level == null || mc.player == null) return resolveClearIcon(time); - - boolean thunder = WorldTweaks.isServerThundering(mc.level); - boolean rain = WorldTweaks.isServerRaining(mc.level); - if (!thunder && !rain) return resolveClearIcon(time); - - boolean day = time < 12000L; - boolean snow = isSnowingAtPlayer(); - if (thunder) { - return snow ? (day ? ICON_THUNDER_SNOW_DAY : ICON_THUNDER_SNOW_NIGHT) - : (day ? ICON_THUNDER_DAY : ICON_THUNDER_NIGHT); - } - if (snow) return day ? ICON_SNOW_DAY : ICON_SNOW_NIGHT; - return day ? ICON_RAIN_DAY : ICON_RAIN_NIGHT; - } - - private int resolveClearIcon(long time) { - if (time >= SUNRISE_START || time < SUNRISE_END) return ICON_SUNRISE; - if (time >= SUNSET_START && time < SUNSET_END) return ICON_SUNSET; - return ICON_SUN; - } - - private boolean isSnowingAtPlayer() { - Minecraft mc = Minecraft.getInstance(); - if (mc == null || mc.level == null || mc.player == null) return false; - BlockPos pos = mc.player.blockPosition(); - Biome.Precipitation precipitation = mc.level.getBiome(pos).value() - .getPrecipitationAt(pos, mc.level.getSeaLevel()); - return precipitation == Biome.Precipitation.SNOW; - } - - private void updatePalette() { - if (isThemeMode()) { - int panelAlpha = bgAlpha.get(); - uiBgPrimary = HudRenderUtil.setAlpha(theme().windowBg(), panelAlpha); - uiBgSecondary = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme().surface(), theme().windowHeader(), 0.35f), - panelAlpha - ); - uiStroke = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme().windowStroke(), theme().strokeSoft(), 0.4f), - Math.min(panelAlpha, 190) - ); - uiIconColor = theme().textPrimary(); - uiValueColor = theme().textPrimary(); - return; - } - - uiBgPrimary = bg.getArgb(); - uiBgSecondary = bg2.getArgb(); - uiStroke = stroke.getArgb(); - uiIconColor = iconColor.getArgb(); - uiValueColor = valueColor.getArgb(); - } - - private boolean isThemeMode() { - return COLOR_THEME.equals(colorMode.get()); - } - - private boolean isCustomMode() { - return COLOR_CUSTOM.equals(colorMode.get()); - } - - private boolean isGlassEffect() { - return EFFECT_GLASS.equals(bgEffect.get()); - } - - private boolean hasEffect() { - return !EFFECT_NONE.equals(bgEffect.get()); - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/SpeedBps.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/SpeedBps.java deleted file mode 100644 index 16282ff..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/SpeedBps.java +++ /dev/null @@ -1,334 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.features.gui.hud.draggable.impl.metrics; - -import dev.loki.lovisual.config.values.color.RGBAColorValue; -import dev.loki.lovisual.config.values.color.RGBColorValue; -import dev.loki.lovisual.config.values.mode.EnumValue; -import dev.loki.lovisual.config.values.mode.ModeValue; -import dev.loki.lovisual.config.values.primitive.BooleanValue; -import dev.loki.lovisual.config.values.primitive.NumberValue; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.resources.Identifier; -import net.minecraft.util.Mth; -import net.minecraft.util.Util; -import dev.loki.lovisual.features.gui.hud.registry.HudElementInfo; -import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; -import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; -import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; -import dev.loki.lovisual.features.gui.hud.script.CompactHudStatModel; -import dev.loki.lovisual.features.gui.hud.script.ScriptableHudStatWidget; -import dev.loki.lovisual.features.gui.hud.script.ScriptedCompactHudStatRenderer; -import dev.loki.lovisual.render.engine.animation.AnimationUtility; -import dev.loki.lovisual.render.engine.math.HudScale; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.text.font.info.FontInfo; -import dev.loki.lovisual.render.engine.text.font.Fonts; -import dev.loki.lovisual.render.engine.text.render.TextRenderer; -import dev.loki.lovisual.util.player.movement.PlayerSpeedUtil; - -import static dev.loki.lovisual.features.theme.Theme.theme; - -//todo Description -@HudElementInfo( - id = "speed_bps", - displayName = "Speed BPS", - enabledByDefault = true, - order = 90 -) -public final class SpeedBps extends DraggableHudElement implements ScriptableHudStatWidget { - - private static final Identifier SPEED_ICON = Identifier.fromNamespaceAndPath("lovisual", "textures/hud/elements/speedometer.png"); - private static final float SCALE_MULT = 0.68f; - private static final float BOX_HEIGHT = 20f; - private static final float BOX_RADIUS = 5f; - private static final float BOX_SOFTNESS = 1.0f; - private static final float STROKE_WIDTH = 0.55f; - private static final float ICON_SIZE = 11f; - private static final float ICON_X = 5f; - private static final float DIVIDER_X = 21f; - private static final float DIVIDER_Y = 4f; - private static final float DIVIDER_WIDTH = 0.6f; - private static final float TEXT_LEFT_X = 26f; - private static final float RIGHT_PAD = 5f; - private static final float KMH_MULT = 3.6f; - private static final String COLOR_THEME = "Theme"; - private static final String COLOR_CUSTOM = "Custom"; - private static final String EFFECT_NONE = "None"; - private static final String EFFECT_BLUR = "Blur"; - private static final String EFFECT_GLASS = "Glass"; - - private static final long SAMPLE_INTERVAL_MS = 75L; - private static final int SAMPLE_COUNT = 20; - - private final HudGlobalConfig hud = HudGlobalConfig.get(); - private final BooleanValue useBps = bool("speed_bps_bps", true); - private final BooleanValue average = bool("speed_bps_average", false); - private final NumberValue scale = num("speed_bps_scale", 2.37, 0.5, 5.0); - private final ModeValue colorMode = mode("speed_bps_color_mode", "color_mode", "Theme", new String[]{COLOR_THEME, COLOR_CUSTOM}); - private final RGBColorValue iconColor = visibleWhen(colorNoAlpha("speed_bps_icon_color", "#FFFFFF"), this::isCustomMode); - private final RGBColorValue valueColor = visibleWhen(colorNoAlpha("speed_bps_value_color", "#FFFFFF"), this::isCustomMode); - private final RGBColorValue metaColor = visibleWhen(colorNoAlpha("speed_bps_meta_color", "#9B9B9B"), this::isCustomMode); - private final ModeValue bgEffect = mode("speed_bps_bg_effect", "bg_effect", "Blur", new String[]{EFFECT_NONE, EFFECT_BLUR, EFFECT_GLASS}); - private final RGBAColorValue bg = visibleWhen(color("speed_bps_bg", "#F7343434"), () -> isCustomMode() && !isGlassEffect()); - private final RGBAColorValue bg2 = visibleWhen(color("speed_bps_bg_secondary", "#F7161616"), () -> isCustomMode() && !isGlassEffect()); - private final RGBColorValue stroke = visibleWhen(colorNoAlpha("speed_bps_stroke", "#5A5A5A"), () -> isCustomMode() && !isGlassEffect()); - private final NumberValue bgAlpha = visibleWhen(num("speed_bps_bg_alpha", 135, 0, 255), () -> isThemeMode() && !isGlassEffect()); - private final NumberValue blurAlpha = visibleWhen(num("speed_bps_blur_alpha", 255, 0, 255), this::hasEffect); - private final EnumValue labelEffect = - enumSetting("speed_bps_label_effect", HudTextEffects.Effect.MIX, - HudTextEffects.Effect.NONE, HudTextEffects.Effect.MIX, HudTextEffects.Effect.FLOW, - HudTextEffects.Effect.PULSE, HudTextEffects.Effect.STRIPE); - private final NumberValue labelEffectSpeed = - visibleWhen(num("speed_bps_label_effect_speed", 60, 1, 60), - () -> labelEffect.get() != HudTextEffects.Effect.NONE); - private final CompactHudStatModel scriptModel = new CompactHudStatModel("speed_bps"); - private final float[] samples = new float[SAMPLE_COUNT]; - private final StringBuilder valueBuilder = new StringBuilder(16); - private int sampleIndex = 0; - private int sampleCount = 0; - private long lastSampleMs = 0L; - private float displayedSpeed = 0f; - private int lastTenth = Integer.MIN_VALUE; - private String cachedNumber = "0.0"; - private float displayValueWidth = -1f; - private int uiBgPrimary; - private int uiBgSecondary; - private int uiStroke; - private int uiIconColor; - private int uiValueColor; - private int uiMetaColor; - - private static void appendOneDecimal(StringBuilder sb, int tenth) { - if (tenth < 0) { - sb.append('-'); - tenth = -tenth; - } - int intPart = tenth / 10; - int frac = tenth - intPart * 10; - sb.append(intPart); - sb.append('.'); - sb.append((char) ('0' + frac)); - } - - private static float smoothWidth(float current, float target) { - if (current < 0.0f) return target; - if (target >= current) return target; - float next = AnimationUtility.approach(current, target, AnimationUtility.deltaTime(), 12.0f); - return AnimationUtility.snap(next, target, 0.25f); - } - - @Override - public void applyDefaultPosition(int screenW, int screenH) { - this.x = 16f; - this.y = screenH - 48f; - } - - @Override - public boolean usesEngineRenderer() { - return true; - } - - @Override - public void renderEngine(Renderer2D renderer, - TextRenderer textRenderer, - GuiGraphicsExtractor ctx, - float tickDelta, - int screenW, - int screenH) { - Minecraft mc = Minecraft.getInstance(); - boolean preview = DraggableHudElementRegistry.isForceVisible(); - if (mc == null && !preview) { - width = 0f; - height = 0f; - scriptModel.setVisible(false); - return; - } - if (!preview && !isEnabled()) { - width = 0f; - height = 0f; - scriptModel.setVisible(false); - return; - } - - float currentBps = preview ? 0f : currentBps(); - updateSamples(currentBps); - - boolean showBps = useBps.get(); - boolean useAverage = average.get(); - float baseBps = useAverage ? averageBps(currentBps) : currentBps; - displayedSpeed = Mth.lerp(0.35f, displayedSpeed, baseBps); - - float valueSpeed = showBps ? displayedSpeed : displayedSpeed * KMH_MULT; - String value = getSpeedNumber(valueSpeed); - String unit = showBps ? "b/s" : "km/h"; - updatePalette(); - - TextRenderer fallback = textRenderer != null ? textRenderer : TextRenderer.get(); - TextRenderer valueRenderer = Fonts.renderer("Onest", FontInfo.Type.Regular, fallback); - TextRenderer metaRenderer = Fonts.renderer("OnestMedium", FontInfo.Type.Regular, valueRenderer); - - float drawScale = HudScale.scale(screenW, screenH) - * (hud.getFontSize() / 18f) - * SCALE_MULT - * this.scale.get().floatValue(); - - valueRenderer.begin(drawScale, true, false); - float valueW = (float) valueRenderer.getWidth(value, false); - float valueH = (float) valueRenderer.getHeight(false); - valueRenderer.end(); - - metaRenderer.begin(drawScale, true, false); - float unitW = (float) metaRenderer.getWidth(unit, false); - float unitH = (float) metaRenderer.getHeight(false); - metaRenderer.end(); - - float boxH = BOX_HEIGHT * drawScale; - float iconX = x + ICON_X * drawScale; - float iconSize = ICON_SIZE * drawScale; - float unitGap = 2f * drawScale; - displayValueWidth = smoothWidth(displayValueWidth, valueW); - float valueX = x + TEXT_LEFT_X * drawScale; - float boxW = (valueX - x) + displayValueWidth + unitGap + unitW + RIGHT_PAD * drawScale; - width = boxW; - height = boxH; - - float baseX = x; - float baseY = y; - float iconY = baseY + (boxH - iconSize) * 0.5f; - float dividerX = baseX + DIVIDER_X * drawScale; - float dividerY = baseY + DIVIDER_Y * drawScale; - float dividerW = Math.max(0.5f, DIVIDER_WIDTH * drawScale); - float dividerH = Math.max(0.5f, boxH - 2f * DIVIDER_Y * drawScale); - float textY = baseY + (boxH - Math.max(valueH, unitH)) * 0.5f; - float unitX = valueX + displayValueWidth + unitGap; - - int iconColor = uiIconColor; - int dividerColor = uiMetaColor; - int valueColor = uiValueColor; - int unitColor = uiMetaColor; - boolean useLabelEffect = labelEffect.get() != HudTextEffects.Effect.NONE; - float time = useLabelEffect ? (float) (Util.getMillis() / 1000.0) : 0.0f; - if (useLabelEffect) { - iconColor = HudTextEffects.animatedColor(iconColor, labelEffect.get(), - labelEffectSpeed.get(), time, 0.0f); - dividerColor = HudTextEffects.animatedColor(dividerColor, labelEffect.get(), - labelEffectSpeed.get(), time, 0.15f); - } - - float radius = BOX_RADIUS * drawScale; - scriptModel.setVisible(true); - scriptModel.setRoot(baseX, baseY, boxW, boxH, radius, drawScale); - scriptModel.background().set(bgEffect.get(), isThemeMode(), blurAlpha.get() / 255.0f, - uiBgPrimary, uiBgSecondary, uiStroke, Math.max(0.5f, STROKE_WIDTH * drawScale), BOX_SOFTNESS); - scriptModel.icon().texture(SPEED_ICON.toString(), iconX - baseX, iconY - baseY, iconSize, iconSize, iconColor); - scriptModel.divider().set(dividerX - baseX, dividerY - baseY, dividerW, dividerH, - HudRenderUtil.setAlpha(dividerColor, 0x52)); - scriptModel.value().set(value, "Onest", drawScale, valueX - baseX, textY - baseY, displayValueWidth, valueColor); - scriptModel.unit().set(unit, "OnestMedium", drawScale, unitX - baseX, textY - baseY, unitW, unitColor); - scriptModel.extra().hidden(); - scriptModel.animation().setLabelEffect(labelEffect.get().name(), labelEffectSpeed.get(), time) - .setDigitAnimation(false, "", 1.0f, 0.0f); - scriptModel.data("bps", currentBps) - .data("displayedBps", displayedSpeed) - .data("average", useAverage) - .data("useBps", showBps) - .data("valueText", value) - .data("unitText", unit); - ScriptedCompactHudStatRenderer.INSTANCE.render(scriptModel, renderer, fallback, ctx, tickDelta); - } - - @Override - public boolean supportsWidgetAnchoring() { - return true; - } - - @Override - public CompactHudStatModel compactStatModel() { - return scriptModel; - } - - private float currentBps() { - Minecraft mc = Minecraft.getInstance(); - return PlayerSpeedUtil.getBps(mc != null ? mc.player : null); - } - - private void updateSamples(float currentBps) { - long now = System.currentTimeMillis(); - if (now - lastSampleMs < SAMPLE_INTERVAL_MS) return; - lastSampleMs = now; - samples[sampleIndex] = currentBps; - sampleIndex = (sampleIndex + 1) % SAMPLE_COUNT; - if (sampleCount < SAMPLE_COUNT) sampleCount++; - } - - private float averageBps(float fallback) { - if (sampleCount <= 0) return fallback; - float sum = 0f; - for (int i = 0; i < sampleCount; i++) { - sum += samples[i]; - } - return sum / sampleCount; - } - - private String getSpeedNumber(float speed) { - int tenth = Math.round(speed * 10f); - if (tenth == lastTenth) return cachedNumber; - lastTenth = tenth; - valueBuilder.setLength(0); - appendOneDecimal(valueBuilder, tenth); - cachedNumber = valueBuilder.toString(); - return cachedNumber; - } - - private void updatePalette() { - if (isThemeMode()) { - int panelAlpha = bgAlpha.get(); - uiBgPrimary = HudRenderUtil.setAlpha(theme().windowBg(), panelAlpha); - uiBgSecondary = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme().surface(), theme().windowHeader(), 0.35f), - panelAlpha - ); - uiStroke = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme().windowStroke(), theme().strokeSoft(), 0.4f), - Math.min(panelAlpha, 190) - ); - uiIconColor = theme().textPrimary(); - uiValueColor = theme().textPrimary(); - uiMetaColor = theme().textMuted(); - return; - } - - uiBgPrimary = bg.getArgb(); - uiBgSecondary = bg2.getArgb(); - uiStroke = stroke.getArgb(); - uiIconColor = iconColor.getArgb(); - uiValueColor = valueColor.getArgb(); - uiMetaColor = metaColor.getArgb(); - } - - private boolean isThemeMode() { - return COLOR_THEME.equals(colorMode.get()); - } - - private boolean isCustomMode() { - return COLOR_CUSTOM.equals(colorMode.get()); - } - - private boolean isGlassEffect() { - return EFFECT_GLASS.equals(bgEffect.get()); - } - - private boolean hasEffect() { - return !EFFECT_NONE.equals(bgEffect.get()); - } -} - diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/SystemTime.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/SystemTime.java deleted file mode 100644 index 511038e..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/SystemTime.java +++ /dev/null @@ -1,282 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.features.gui.hud.draggable.impl.metrics; - -import dev.loki.lovisual.config.values.color.RGBAColorValue; -import dev.loki.lovisual.config.values.color.RGBColorValue; -import dev.loki.lovisual.config.values.mode.EnumValue; -import dev.loki.lovisual.config.values.mode.ModeValue; -import dev.loki.lovisual.config.values.primitive.BooleanValue; -import dev.loki.lovisual.config.values.primitive.NumberValue; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.util.Util; -import dev.loki.lovisual.features.gui.hud.registry.HudElementInfo; -import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; -import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; -import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; -import dev.loki.lovisual.features.gui.hud.script.CompactHudStatModel; -import dev.loki.lovisual.features.gui.hud.script.ScriptableHudStatWidget; -import dev.loki.lovisual.features.gui.hud.script.ScriptedCompactHudStatRenderer; -import dev.loki.lovisual.render.engine.animation.AnimationUtility; -import dev.loki.lovisual.render.engine.math.HudScale; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.text.font.info.FontInfo; -import dev.loki.lovisual.render.engine.text.font.Fonts; -import dev.loki.lovisual.render.engine.text.render.TextRenderer; - -import java.time.LocalTime; - -import static dev.loki.lovisual.features.theme.Theme.theme; - -@HudElementInfo( - id = "system_time", - displayName = "System Time", - enabledByDefault = false, - order = 170 -) -public final class SystemTime extends DraggableHudElement implements ScriptableHudStatWidget { - - private static final float SCALE_MULT = 0.68f; - private static final float BOX_HEIGHT = 20f; - private static final float BOX_RADIUS = 5f; - private static final float BOX_SOFTNESS = 1.0f; - private static final float STROKE_WIDTH = 0.55f; - private static final float ICON_X = 5f; - private static final float DIVIDER_X = 21f; - private static final float DIVIDER_Y = 4f; - private static final float DIVIDER_WIDTH = 0.6f; - private static final float TEXT_LEFT_X = 26f; - private static final float RIGHT_PAD = 5f; - private static final float ICON_SCALE = 0.88f; - private static final String WIDTH_TEMPLATE = "00:00"; - private static final String WIDTH_TEMPLATE_SECONDS = "00:00:00"; - private static final String COLOR_THEME = "Theme"; - private static final String COLOR_CUSTOM = "Custom"; - private static final String EFFECT_NONE = "None"; - private static final String EFFECT_BLUR = "Blur"; - private static final String EFFECT_GLASS = "Glass"; - private static final String[] CLOCK_ICONS = { - iconString(0xEA10), - iconString(0xEA14), - iconString(0xEA15), - iconString(0xEA16), - iconString(0xEA17), - iconString(0xEA18), - iconString(0xEA19), - iconString(0xEA1A), - iconString(0xEA10), - iconString(0xEA11), - iconString(0xEA12), - iconString(0xEA13) - }; - - private final HudGlobalConfig hud = HudGlobalConfig.get(); - private final BooleanValue showSeconds = bool("system_time_seconds", false); - private final NumberValue scale = num("system_time_scale", 2.37, 0.5, 5.0); - private final ModeValue colorMode = mode("system_time_color_mode", "color_mode", "Theme", new String[]{COLOR_THEME, COLOR_CUSTOM}); - private final RGBColorValue iconColor = visibleWhen(colorNoAlpha("system_time_icon_color", "#FFFFFF"), this::isCustomMode); - private final RGBColorValue valueColor = visibleWhen(colorNoAlpha("system_time_value_color", "#FFFFFF"), this::isCustomMode); - private final ModeValue bgEffect = mode("system_time_bg_effect", "bg_effect", "Blur", new String[]{EFFECT_NONE, EFFECT_BLUR, EFFECT_GLASS}); - private final RGBAColorValue bg = visibleWhen(color("system_time_bg", "#EB111318"), () -> isCustomMode() && !isGlassEffect()); - private final RGBAColorValue bg2 = visibleWhen(color("system_time_bg_secondary", "#F7161616"), () -> isCustomMode() && !isGlassEffect()); - private final RGBColorValue stroke = visibleWhen(colorNoAlpha("system_time_stroke", "#5A5A5A"), () -> isCustomMode() && !isGlassEffect()); - private final NumberValue bgAlpha = visibleWhen(num("system_time_bg_alpha", 235, 0, 255), () -> isThemeMode() && !isGlassEffect()); - private final NumberValue blurAlpha = visibleWhen(num("system_time_blur_alpha", 140, 0, 255), this::hasEffect); - private final EnumValue iconEffect = - enumSetting("system_time_icon_effect", HudTextEffects.Effect.NONE, - HudTextEffects.Effect.NONE, HudTextEffects.Effect.MIX, HudTextEffects.Effect.FLOW, - HudTextEffects.Effect.PULSE, HudTextEffects.Effect.STRIPE); - private final NumberValue iconEffectSpeed = - visibleWhen(num("system_time_icon_effect_speed", 18, 1, 60), - () -> iconEffect.get() != HudTextEffects.Effect.NONE); - private final CompactHudStatModel scriptModel = new CompactHudStatModel("system_time"); - - private int uiBgPrimary; - private int uiBgSecondary; - private int uiStroke; - private int uiIconColor; - private int uiValueColor; - private float displayTimeWidth = -1f; - - private static String iconString(int codepoint) { - return new String(Character.toChars(codepoint)); - } - - private static TextRenderer getWeatherIcons() { - return Fonts.renderer("WeatherIcons", FontInfo.Type.Regular, TextRenderer.get()); - } - - private static String formatTime(LocalTime time, boolean showSeconds) { - if (showSeconds) { - return String.format("%02d:%02d:%02d", time.getHour(), time.getMinute(), time.getSecond()); - } - return String.format("%02d:%02d", time.getHour(), time.getMinute()); - } - - private static float smoothWidth(float current, float target) { - if (current < 0.0f) return target; - if (target >= current) return target; - float next = AnimationUtility.approach(current, target, AnimationUtility.deltaTime(), 12.0f); - return AnimationUtility.snap(next, target, 0.25f); - } - - @Override - public void applyDefaultPosition(int screenW, int screenH) { - this.x = 16f; - this.y = 48f; - } - - @Override - public boolean usesEngineRenderer() { - return true; - } - - @Override - public void renderEngine(Renderer2D renderer, - TextRenderer textRenderer, - GuiGraphicsExtractor ctx, - float tickDelta, - int screenW, - int screenH) { - boolean preview = DraggableHudElementRegistry.isForceVisible(); - if (Minecraft.getInstance() == null && !preview) { - width = 0f; - height = 0f; - scriptModel.setVisible(false); - return; - } - if (!preview && !isEnabled()) { - width = 0f; - height = 0f; - scriptModel.setVisible(false); - return; - } - - LocalTime now = LocalTime.now(); - String icon = CLOCK_ICONS[now.getHour() % 12]; - String timeText = formatTime(now, showSeconds.get()); - updatePalette(); - - TextRenderer fallback = textRenderer != null ? textRenderer : TextRenderer.get(); - TextRenderer iconRenderer = getWeatherIcons(); - if (iconRenderer == null) iconRenderer = fallback; - TextRenderer valueRenderer = Fonts.renderer("Onest", FontInfo.Type.Regular, fallback); - - float drawScale = HudScale.scale(screenW, screenH) - * (hud.getFontSize() / 18f) - * SCALE_MULT - * scale.get().floatValue(); - - float iconScale = drawScale * ICON_SCALE; - iconRenderer.begin(iconScale, true, false); - float iconW = (float) iconRenderer.getWidth(icon, false); - float iconH = (float) iconRenderer.getHeight(false); - iconRenderer.end(); - - valueRenderer.begin(drawScale, true, false); - float timeW = (float) valueRenderer.getWidth(timeText, false); - float timeH = (float) valueRenderer.getHeight(false); - String widthTemplate = showSeconds.get() ? WIDTH_TEMPLATE_SECONDS : WIDTH_TEMPLATE; - float reservedTimeW = (float) valueRenderer.getWidth(widthTemplate, false); - valueRenderer.end(); - - displayTimeWidth = Math.max(smoothWidth(displayTimeWidth, timeW), reservedTimeW); - float boxH = BOX_HEIGHT * drawScale; - float boxW = TEXT_LEFT_X * drawScale + displayTimeWidth + RIGHT_PAD * drawScale; - width = boxW; - height = boxH; - - float baseX = x; - float baseY = y; - float iconX = baseX + ICON_X * drawScale; - float iconY = baseY + (boxH - iconH) * 0.5f; - float dividerX = baseX + DIVIDER_X * drawScale; - float dividerY = baseY + DIVIDER_Y * drawScale; - float dividerW = Math.max(0.5f, DIVIDER_WIDTH * drawScale); - float dividerH = Math.max(0.5f, boxH - 2f * DIVIDER_Y * drawScale); - float textX = baseX + TEXT_LEFT_X * drawScale; - float textY = baseY + (boxH - timeH) * 0.5f; - - int resolvedIconColor = uiIconColor; - boolean useIconEffect = iconEffect.get() != HudTextEffects.Effect.NONE; - float nowSec = useIconEffect ? (float) (Util.getMillis() / 1000.0) : 0.0f; - if (useIconEffect) { - resolvedIconColor = HudTextEffects.animatedColor(resolvedIconColor, iconEffect.get(), - iconEffectSpeed.get(), nowSec, 0.0f); - } - - float radius = BOX_RADIUS * drawScale; - scriptModel.setVisible(true); - scriptModel.setRoot(baseX, baseY, boxW, boxH, radius, drawScale); - scriptModel.background().set(bgEffect.get(), isThemeMode(), blurAlpha.get() / 255.0f, - uiBgPrimary, uiBgSecondary, uiStroke, Math.max(0.5f, STROKE_WIDTH * drawScale), BOX_SOFTNESS); - scriptModel.icon().glyph(icon, "WeatherIcons", iconX - baseX, iconY - baseY, iconW, iconH, iconScale, resolvedIconColor); - scriptModel.divider().set(dividerX - baseX, dividerY - baseY, dividerW, dividerH, - HudRenderUtil.setAlpha(uiValueColor, 0x52)); - scriptModel.value().set(timeText, "Onest", drawScale, textX - baseX, textY - baseY, displayTimeWidth, uiValueColor); - scriptModel.unit().hidden(); - scriptModel.extra().hidden(); - scriptModel.animation().setLabelEffect(iconEffect.get().name(), iconEffectSpeed.get(), nowSec) - .setDigitAnimation(false, "", 1.0f, 0.0f); - scriptModel.data("timeText", timeText) - .data("clockIcon", icon) - .data("showSeconds", showSeconds.get()) - .data("hour", now.getHour()) - .data("minute", now.getMinute()) - .data("second", now.getSecond()); - ScriptedCompactHudStatRenderer.INSTANCE.render(scriptModel, renderer, fallback, ctx, tickDelta); - } - - @Override - public CompactHudStatModel compactStatModel() { - return scriptModel; - } - - private void updatePalette() { - if (isThemeMode()) { - int panelAlpha = bgAlpha.get(); - uiBgPrimary = HudRenderUtil.setAlpha(theme().windowBg(), panelAlpha); - uiBgSecondary = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme().surface(), theme().windowHeader(), 0.35f), - panelAlpha - ); - uiStroke = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme().windowStroke(), theme().strokeSoft(), 0.4f), - Math.min(panelAlpha, 190) - ); - uiIconColor = theme().textPrimary(); - uiValueColor = theme().textPrimary(); - return; - } - - uiBgPrimary = bg.getArgb(); - uiBgSecondary = bg2.getArgb(); - uiStroke = stroke.getArgb(); - uiIconColor = iconColor.getArgb(); - uiValueColor = valueColor.getArgb(); - } - - private boolean isThemeMode() { - return COLOR_THEME.equals(colorMode.get()); - } - - private boolean isCustomMode() { - return COLOR_CUSTOM.equals(colorMode.get()); - } - - private boolean isGlassEffect() { - return EFFECT_GLASS.equals(bgEffect.get()); - } - - private boolean hasEffect() { - return !EFFECT_NONE.equals(bgEffect.get()); - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/Keybinds.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/Keybinds.java deleted file mode 100644 index a334451..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/Keybinds.java +++ /dev/null @@ -1,611 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.features.gui.hud.draggable.impl.panels; - -import dev.loki.lovisual.config.values.color.RGBAColorValue; -import dev.loki.lovisual.config.values.color.RGBColorValue; -import dev.loki.lovisual.config.values.mode.BindMode; -import dev.loki.lovisual.config.values.mode.ModeValue; -import dev.loki.lovisual.config.values.primitive.BooleanMapValue; -import dev.loki.lovisual.config.values.primitive.BooleanValue; -import dev.loki.lovisual.config.values.primitive.NumberValue; -import dev.loki.lovisual.util.screen.ClientScreen; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.client.gui.screens.ChatScreen; -import net.minecraft.util.Util; -import dev.loki.lovisual.config.definition.SettingDef; -import dev.loki.lovisual.features.gui.hud.registry.HudElementRegister; -import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; -import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; -import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; -import dev.loki.lovisual.features.gui.clickgui.settings.bind.FunctionBindSetting; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; -import dev.loki.lovisual.features.module.core.Module; -import dev.loki.lovisual.features.module.core.ModuleCategory; -import dev.loki.lovisual.features.module.core.ModuleManager; -import dev.loki.lovisual.render.engine.animation.AnimationUtility; -import dev.loki.lovisual.render.engine.math.HudScale; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.text.font.info.FontInfo; -import dev.loki.lovisual.render.engine.text.font.Fonts; -import dev.loki.lovisual.render.engine.text.render.TextRenderer; - -import java.util.LinkedHashMap; -import java.util.*; - -import static dev.loki.lovisual.features.theme.Theme.theme; -import dev.loki.lovisual.features.gui.hud.draggable.impl.HudPanelLayoutModes; -import dev.loki.lovisual.features.gui.hud.draggable.impl.scripted.ScriptedListHudPanel; - -@HudElementRegister(order = 45) -public final class Keybinds extends DraggableHudElement { - - { - defaultLinkedLayout(456.6228f, 248.0f, "potions", "RIGHT", 0.0f, 0.0f); - } - - private static final float HEADER_HEIGHT = 15.5f; - private static final float CONTENT_START_Y = 25.0f; - private static final float ROW_STEP = 11.0f; - private static final float MIN_WIDTH = 102.0f; - private static final float HEADER_ICON_SCALE = 1.18f; - private static final float TITLE_TEXT_X = 22.0f; - private static final float COUNT_LABEL_OFFSET = 22.0f; - private static final String COLOR_THEME = "Theme"; - private static final String COLOR_CUSTOM = "Custom"; - private static final String EFFECT_NONE = "None"; - private static final String EFFECT_BLUR = "Blur"; - private static final String MODE_PRESS = "Press"; - private static final String MODE_HOLD = "Hold"; - - private final HudGlobalConfig hud = HudGlobalConfig.get(); - private final ScriptedListHudPanel scriptedPanel = new ScriptedListHudPanel(); - private final NumberValue scaleValue = - new NumberValue<>("keybinds_scale", 1.0, HudPanelLayoutModes.SCALE_MIN, HudPanelLayoutModes.SCALE_MAX); - private final ModeValue layoutMode = - new ModeValue("keybinds_layout", "Unified Divider", HudPanelLayoutModes.SPLIT_HEADER, HudPanelLayoutModes.UNIFIED_DIVIDER); - private final ModeValue colorMode = - new ModeValue("keybinds_color_mode", "Theme", COLOR_THEME, COLOR_CUSTOM); - private final ModeValue bgEffect = - new ModeValue("keybinds_bg_effect", "None", EFFECT_NONE, EFFECT_BLUR); - private final RGBAColorValue bg = - new RGBAColorValue("keybinds_bg", "#F7343434"); - private final RGBAColorValue bg2 = - new RGBAColorValue("keybinds_bg_secondary", "#F7161616"); - private final NumberValue bgAlpha = - new NumberValue<>("keybinds_bg_alpha", 225, 0, 255); - private final RGBColorValue stroke = - new RGBColorValue("keybinds_stroke", "#5A5A5A"); - private final RGBColorValue text = - new RGBColorValue("keybinds_text", "#FFFFFF"); - private final RGBColorValue muted = - new RGBColorValue("keybinds_muted", "#A5A5A5"); - private final NumberValue blurAlpha = - new NumberValue<>("keybinds_blur_alpha", 140, 0, 255); - private final BooleanValue localActions = - new BooleanValue("keybinds_local_actions", true); - private final BooleanMapValue localActionModes = - new BooleanMapValue("keybinds_local_action_modes", new LinkedHashMap<>() {{ - put(MODE_PRESS, true); - put(MODE_HOLD, true); - }}); - private final BooleanValue headerIconPulse = - new BooleanValue("keybinds_header_icon_pulse", true); - private final NumberValue headerIconPulseSpeed = - new NumberValue<>("keybinds_header_icon_pulse_speed", 18, 1, 60); - private final NumberValue headerIconPulseIntensity = - new NumberValue<>("keybinds_header_icon_pulse_intensity", 100, 0, 100); - - private final Map entryAnim = new LinkedHashMap<>(); - private final Map lastEntries = new LinkedHashMap<>(); - private float displayWidth = -1.0f; - private float displayHeight = -1.0f; - private float visibilityAnim = 0.0f; - private int uiHeaderLeft; - private int uiHeaderRight; - private int uiBodyLeft; - private int uiBodyRight; - private int uiOutline; - private int uiText; - private int uiMuted; - private int uiCounter; - private int uiTitleText; - private int uiDivider; - private int uiBlurTint; - - public Keybinds() { - super("keybinds", "Keybinds", true); - } - - private static String iconFor(ModuleCategory category) { - if (category == null) return "ellipsis"; - return switch (category) { - case COMBAT -> "swords"; - case MOVEMENT -> "accessibility"; - case PLAYER -> "user"; - case VISUALS -> "tree-pine"; - default -> "ellipsis"; - }; - } - - private static String cleanActionLabel(FunctionBindSetting setting) { - if (setting == null) return ""; - String explicit = setting.getHudLabel(); - if (explicit != null && !explicit.isBlank()) return explicit; - - String actionId = setting.getActionId(); - String display = setting.getDisplayName(); - if (display == null || display.isBlank() || display.equals("Function: " + actionId)) { - display = actionId; - } - display = display.replace("Function: ", "").replace('_', ' ').replace('-', ' ').trim(); - if (display.isEmpty()) return ""; - - StringBuilder out = new StringBuilder(display.length()); - boolean cap = true; - for (int i = 0; i < display.length(); i++) { - char c = display.charAt(i); - if (Character.isWhitespace(c)) { - if (!out.isEmpty() && out.charAt(out.length() - 1) != ' ') out.append(' '); - cap = true; - continue; - } - out.append(cap ? Character.toUpperCase(c) : c); - cap = false; - } - return out.toString(); - } - - private static String formatBind(String raw) { - if (raw == null || raw.isBlank()) return ""; - String[] parts = raw.trim().toUpperCase(Locale.ROOT).split("\\+"); - List out = new ArrayList<>(parts.length); - for (String part : parts) { - String mapped = formatBindPart(part.trim()); - if (!mapped.isEmpty()) out.add(mapped); - } - return String.join("+", out); - } - - private static String formatBindPart(String value) { - return switch (value) { - case "LEFT_CONTROL", "RIGHT_CONTROL", "LEFT_CTRL", "RIGHT_CTRL", "CONTROL", "CTRL" -> "CTRL"; - case "LEFT_SHIFT", "RIGHT_SHIFT", "SHIFT" -> "SHIFT"; - case "LEFT_ALT", "RIGHT_ALT", "ALT" -> "ALT"; - case "LEFT_SUPER", "RIGHT_SUPER", "SUPER" -> "WIN"; - case "GRAVE_ACCENT" -> "`"; - case "COMMA" -> ","; - case "PERIOD" -> "."; - case "SEMICOLON" -> ";"; - case "APOSTROPHE" -> "'"; - case "SLASH" -> "/"; - case "BACKSLASH" -> "\\"; - case "MINUS" -> "-"; - case "EQUAL" -> "="; - case "SPACE" -> "SPC"; - case "ENTER" -> "ENT"; - case "ESCAPE" -> "ESC"; - case "DELETE" -> "DEL"; - case "INSERT" -> "INS"; - case "CAPS_LOCK" -> "CAPS"; - case "LEFT_BRACKET" -> "["; - case "RIGHT_BRACKET" -> "]"; - default -> value.startsWith("MOUSE_BUTTON_") ? "M" + value.substring("MOUSE_BUTTON_".length()) : value; - }; - } - - private static int clamp255(int value) { - return Math.max(0, Math.min(255, value)); - } - - @Override - protected void defineSettings(List defs) { - defs.add(SettingDef.number(scaleValue)); - defs.add(SettingDef.mode(layoutMode)); - defs.add(SettingDef.mode(colorMode)); - defs.add(SettingDef.color(bg).visibleWhen(this::isCustomMode)); - defs.add(SettingDef.color(bg2).visibleWhen(this::isCustomMode)); - defs.add(SettingDef.colorNoAlpha(stroke).visibleWhen(this::isCustomMode)); - defs.add(SettingDef.number(bgAlpha).visibleWhen(this::isThemeMode)); - defs.add(SettingDef.colorNoAlpha(text).visibleWhen(this::isCustomMode)); - defs.add(SettingDef.colorNoAlpha(muted).visibleWhen(this::isCustomMode)); - defs.add(SettingDef.mode(bgEffect)); - defs.add(SettingDef.number(blurAlpha).visibleWhen(this::hasEffect)); - defs.add(SettingDef.bool(localActions)); - defs.add(SettingDef.group("keybinds_local_action_modes", localActionModes).visibleWhen(localActions::get)); - defs.add(SettingDef.bool(headerIconPulse)); - defs.add(SettingDef.number(headerIconPulseSpeed).visibleWhen(headerIconPulse::get)); - defs.add(SettingDef.number(headerIconPulseIntensity).visibleWhen(headerIconPulse::get)); - } - - @Override - public void applyDefaultPosition(int screenW, int screenH) { - this.x = screenW - 128.0f; - this.y = 80.0f; - } - - @Override - public boolean usesEngineRenderer() { - return true; - } - - @Override - public void renderEngine(Renderer2D renderer, - TextRenderer textRenderer, - GuiGraphicsExtractor ctx, - float tickDelta, - int screenW, - int screenH) { - boolean forceVisible = DraggableHudElementRegistry.isForceVisible(); - Minecraft mc = Minecraft.getInstance(); - if (mc == null || (mc.player == null && !forceVisible)) { - width = 0.0f; - height = 0.0f; - return; - } - - boolean chatPreview = ClientScreen.current() instanceof ChatScreen; - List rows = collectRows(); - List animatedRows = animateRows(rows); - boolean showExampleRow = rows.isEmpty() && (forceVisible || chatPreview); - boolean showWidget = !rows.isEmpty() || showExampleRow; - visibilityAnim = HudRenderUtil.animateVisibility(visibilityAnim, showWidget); - float widgetScale = HudRenderUtil.visibilityScale(visibilityAnim); - if (animatedRows.isEmpty() && !showExampleRow && visibilityAnim <= 0.0f) { - width = 0.0f; - height = 0.0f; - return; - } - - updatePalette(); - - TextRenderer headerIconRenderer = Fonts.renderer("Icons", FontInfo.Type.Regular, TextRenderer.get()); - TextRenderer headerTextRenderer = Fonts.renderer("Inter", FontInfo.Type.Bold, TextRenderer.get()); - TextRenderer rowTextRenderer = Fonts.renderer("OnestMedium", FontInfo.Type.Regular, TextRenderer.get()); - if (headerIconRenderer == null) headerIconRenderer = textRenderer; - if (headerTextRenderer == null) headerTextRenderer = textRenderer; - if (rowTextRenderer == null) rowTextRenderer = textRenderer; - - float baseScale = HudScale.scale(screenW, screenH) * 1.1f * HudPanelLayoutModes.effectiveScale(scaleValue); - float rowStep = ROW_STEP * baseScale; - float fontScale = 0.98f * (hud.getFontSize() / 18.0f); - - headerIconRenderer.begin(fontScale * HEADER_ICON_SCALE, true, false); - float headerIconH = (float) headerIconRenderer.getHeight(false); - headerIconRenderer.end(); - - headerTextRenderer.begin(fontScale, true, false); - float headerTextH = (float) headerTextRenderer.getHeight(false); - float titleWidth = (float) headerTextRenderer.getWidth("Keybinds", false); - headerTextRenderer.end(); - - rowTextRenderer.begin(fontScale, true, false); - float rowTextH = (float) rowTextRenderer.getHeight(false); - float maxWidth = MIN_WIDTH * baseScale; - if (showExampleRow) { - float previewWidth = (float) rowTextRenderer.getWidth("KillAuraG", false) + (34.0f * baseScale); - maxWidth = Math.max(maxWidth, previewWidth); - } else { - for (AnimatedRow animatedRow : animatedRows) { - Row row = animatedRow.row(); - float widthCandidate = (float) rowTextRenderer.getWidth(row.name() + row.bindText(), false) + (34.0f * baseScale); - maxWidth = Math.max(maxWidth, widthCandidate); - } - } - rowTextRenderer.end(); - - rowTextRenderer.begin(fontScale * 0.92f, false, false); - String activeCountText = Integer.toString(rows.size()); - float activeLabelWidth = (float) rowTextRenderer.getWidth("Active:", false); - float activeCountWidth = (float) rowTextRenderer.getWidth(activeCountText, false); - rowTextRenderer.end(); - - float headerWidth = (TITLE_TEXT_X * baseScale) + titleWidth - + activeLabelWidth + activeCountWidth - + ((COUNT_LABEL_OFFSET + 12.0f) * baseScale); - maxWidth = Math.max(maxWidth, headerWidth); - - float contentHeight = showExampleRow ? rowStep : totalAnimatedHeight(animatedRows, rowStep); - float targetHeight = (showExampleRow || !animatedRows.isEmpty()) - ? (CONTENT_START_Y * baseScale + contentHeight) - : (HEADER_HEIGHT * baseScale); - - displayWidth = HudRenderUtil.animateDimension(displayWidth, maxWidth); - displayHeight = HudRenderUtil.animateDimension(displayHeight, targetHeight); - width = displayWidth; - height = displayHeight; - - if (!showWidget && widgetScale <= 0.001f && animatedRows.isEmpty()) { - width = 0.0f; - height = 0.0f; - return; - } - - float drawScale = Math.max(0.0f, widgetScale); - float drawWidth = displayWidth * drawScale; - float drawHeight = displayHeight * drawScale; - if (drawWidth <= 0.0f || drawHeight <= 0.0f) { - width = 0.0f; - height = 0.0f; - return; - } - - float drawX = x + ((displayWidth - drawWidth) * 0.5f); - float drawY = y + ((displayHeight - drawHeight) * 0.5f); - float drawBaseScale = baseScale * drawScale; - float drawFontScale = fontScale * drawScale; - float drawHeaderIconH = headerIconH * drawScale; - float drawHeaderTextH = headerTextH * drawScale; - float drawRowTextH = rowTextH * drawScale; - int resolvedHeaderIconColor = uiCounter; - if (headerIconPulse.get()) { - int pulsedHeaderIconColor = HudTextEffects.animatedColor( - uiCounter, - HudTextEffects.Effect.PULSE, - headerIconPulseSpeed.get(), - (float) (Util.getMillis() / 1000.0), - 0.0f - ); - resolvedHeaderIconColor = HudRenderUtil.mixColor( - uiCounter, - pulsedHeaderIconColor, - headerIconPulseIntensity.get() / 100.0f - ); - } - - List> panelRows = ScriptedListHudPanel.rows(); - if (showExampleRow) { - LinkedHashMap row = ScriptedListHudPanel.row( - "preview", - "svg", - "swords", - List.of(ScriptedListHudPanel.textPart("KillAura", withAlpha(uiText & 0x00FFFFFF, 255), 0.0f)), - "G", - withAlpha(uiCounter & 0x00FFFFFF, 255), - withAlpha(uiMuted & 0x00FFFFFF, 130), - 1.0f - ); - row.put("iconTint", ScriptedListHudPanel.hex(withAlpha(uiCounter & 0x00FFFFFF, 255))); - panelRows.add(row); - } else { - for (AnimatedRow animatedRow : animatedRows) { - float anim = animatedRow.anim(); - if (anim <= 0.01f) continue; - Row rowData = animatedRow.row(); - int rowAlpha = clamp255(Math.round(255.0f * anim)); - int rowNameColor = rowData.local() - ? withAlpha(uiCounter & 0x00FFFFFF, rowAlpha) - : withAlpha(uiText & 0x00FFFFFF, rowAlpha); - int rowRightColor = withAlpha(uiCounter & 0x00FFFFFF, rowAlpha); - int rowDividerColor = rowData.local() - ? withAlpha(uiCounter & 0x00FFFFFF, Math.max(42, rowAlpha - 94)) - : withAlpha(uiMuted & 0x00FFFFFF, Math.max(26, rowAlpha - 120)); - LinkedHashMap row = ScriptedListHudPanel.row( - rowData.key(), - "svg", - rowData.icon(), - List.of(ScriptedListHudPanel.textPart(rowData.name(), rowNameColor, 0.0f)), - rowData.bindText(), - rowRightColor, - rowDividerColor, - anim - ); - row.put("iconTint", ScriptedListHudPanel.hex(withAlpha(uiCounter & 0x00FFFFFF, rowAlpha))); - panelRows.add(row); - } - } - - scriptedPanel.render( - renderer, - textRenderer, - ctx, - tickDelta, - new ScriptedListHudPanel.Panel( - ScriptedListHudPanel.KEYBINDS, - new ScriptedListHudPanel.Palette(uiHeaderLeft, uiHeaderRight, uiBodyLeft, uiBodyRight, - uiOutline, uiText, uiMuted, uiCounter, uiTitleText, uiDivider, uiBlurTint), - drawX, - drawY, - drawWidth, - drawHeight, - drawScale, - drawBaseScale, - drawFontScale, - drawHeaderIconH, - drawHeaderTextH, - drawRowTextH, - activeLabelWidth * drawScale, - activeCountWidth * drawScale, - rows.size(), - hasEffect(), - Math.min(1.0f, (blurAlpha.get() / 255.0f) * (isThemeMode() ? 1.15f : 1.0f)), - resolvedHeaderIconColor, - HudPanelLayoutModes.current(layoutMode), - panelRows - ) - ); - } - - private List collectRows() { - List rows = new ArrayList<>(); - for (Module module : ModuleManager.getModules()) { - if (module == null || !module.isEnabled()) continue; - String name = module.getDisplayName(); - if (name == null || name.isBlank()) name = module.name(); - String moduleIcon = iconFor(module.getCategory()); - - if (module.getKeyBindSetting() != null && module.getKeyBindSetting().getValue() != null) { - String rawBind = module.getKeyBindSetting().getValue().get(); - if (rawBind != null && !rawBind.isBlank() && !"NONE".equalsIgnoreCase(rawBind)) { - rows.add(new Row( - module.name(), - name, - formatBind(rawBind), - moduleIcon, - false - )); - } - } - - for (FunctionBindSetting action : module.getActionSettings()) { - if (action == null || !isLocalActionVisible(action)) continue; - String rawBind = action.get(); - if (rawBind == null || rawBind.isBlank() || "NONE".equalsIgnoreCase(rawBind)) continue; - - String actionLabel = cleanActionLabel(action); - if (actionLabel.isBlank()) actionLabel = action.getActionId(); - String actionIcon = action.getHudIcon(); - if (actionIcon == null || actionIcon.isBlank()) actionIcon = moduleIcon; - - rows.add(new Row( - module.name() + ":action:" + action.getActionId(), - name + ": " + actionLabel, - formatBind(rawBind), - actionIcon, - true - )); - } - } - rows.sort((a, b) -> String.CASE_INSENSITIVE_ORDER.compare(a.name(), b.name())); - return rows; - } - - private boolean isLocalActionVisible(FunctionBindSetting action) { - if (!localActions.get()) return false; - BindMode mode = action.getMode(); - if (mode == BindMode.HOLD) { - return localActionModes.get(MODE_HOLD) && action.isHeldForHud(); - } - return localActionModes.get(MODE_PRESS) && action.isHudToggle() && action.isHudToggleActive(); - } - - private List animateRows(List rows) { - Map current = new LinkedHashMap<>(); - for (Row row : rows) { - current.put(row.key(), row); - } - - Map merged = new LinkedHashMap<>(); - merged.putAll(current); - for (Map.Entry entry : lastEntries.entrySet()) { - merged.putIfAbsent(entry.getKey(), entry.getValue()); - } - lastEntries.clear(); - lastEntries.putAll(merged); - - List toRemove = new ArrayList<>(); - for (Map.Entry entry : lastEntries.entrySet()) { - String key = entry.getKey(); - float target = current.containsKey(key) ? 1.0f : 0.0f; - float anim = entryAnim.getOrDefault(key, 0.0f); - anim = AnimationUtility.approach(anim, target, AnimationUtility.deltaTime(), 12.0f); - anim = AnimationUtility.snap(anim, target, 0.02f); - if (anim <= 0.0f && target <= 0.0f) { - toRemove.add(key); - } else { - entryAnim.put(key, anim); - } - } - for (String key : toRemove) { - entryAnim.remove(key); - lastEntries.remove(key); - } - - List out = new ArrayList<>(); - for (Map.Entry entry : lastEntries.entrySet()) { - float anim = entryAnim.getOrDefault(entry.getKey(), 0.0f); - if (anim > 0.01f) { - out.add(new AnimatedRow(entry.getValue(), anim)); - } - } - return out; - } - - private float totalAnimatedHeight(List rows, float rowStep) { - float out = 0.0f; - for (AnimatedRow row : rows) { - out += rowStep * row.anim(); - } - return out; - } - - private void updatePalette() { - if (isThemeMode()) { - int alpha = bgAlpha.get(); - int window = HudRenderUtil.setAlpha(theme().windowBg(), alpha); - int header = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme().windowHeader(), theme().surface(), 0.18f), - Math.min(255, alpha + 14) - ); - int surface = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme().surface(), theme().windowBg(), 0.22f), - Math.min(255, alpha + 6) - ); - int deep = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme().surface(), theme().windowHeader(), 0.42f), - Math.min(255, alpha + 18) - ); - - uiHeaderLeft = HudRenderUtil.mixColor(header, window, 0.22f); - uiHeaderRight = HudRenderUtil.mixColor(surface, header, 0.52f); - uiBodyLeft = HudRenderUtil.mixColor(window, surface, 0.24f); - uiBodyRight = HudRenderUtil.mixColor(deep, surface, 0.18f); - uiOutline = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme().windowStroke(), theme().strokeSoft(), 0.18f), - Math.min(255, Math.max(182, alpha + 36)) - ); - uiText = theme().textPrimary(); - uiTitleText = HudRenderUtil.mixColor(theme().textPrimary(), theme().accent(), 0.14f); - uiMuted = HudRenderUtil.mixColor(theme().textMuted(), theme().textPrimary(), 0.16f); - uiCounter = HudRenderUtil.mixColor(theme().accent(), theme().textPrimary(), 0.38f); - uiDivider = HudRenderUtil.scaleAlpha(uiMuted, 0.68f); - uiBlurTint = HudRenderUtil.mixColor( - HudRenderUtil.mixColor(uiHeaderRight, uiBodyRight, 0.5f), - theme().accent(), - 0.14f - ); - return; - } - - uiHeaderLeft = bg.getArgb(); - uiHeaderRight = bg2.getArgb(); - uiBodyLeft = HudRenderUtil.mixColor(bg.getArgb(), bg2.getArgb(), 0.15f); - uiBodyRight = bg2.getArgb(); - uiOutline = stroke.getArgb(); - uiText = text.getArgb(); - uiTitleText = uiText; - uiMuted = muted.getArgb(); - uiCounter = HudRenderUtil.mixColor(text.getArgb(), 0xFFE1E1FF, 0.20f); - uiDivider = HudRenderUtil.scaleAlpha(uiMuted, 0.55f); - uiBlurTint = HudRenderUtil.mixColor(uiHeaderLeft, uiBodyRight, 0.5f); - } - - private int withAlpha(int rgb, int alpha) { - return (clamp255(alpha) << 24) | (rgb & 0x00FFFFFF); - } - - private boolean isThemeMode() { - return COLOR_THEME.equals(colorMode.get()); - } - - private boolean isCustomMode() { - return COLOR_CUSTOM.equals(colorMode.get()); - } - - private boolean hasEffect() { - return !EFFECT_NONE.equals(bgEffect.get()); - } - - private record Row(String key, String name, String bindText, String icon, boolean local) { - } - - private record AnimatedRow(Row row, float anim) { - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/ModuleList.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/ModuleList.java deleted file mode 100644 index a001389..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/ModuleList.java +++ /dev/null @@ -1,499 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.features.gui.hud.draggable.impl.panels; - -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.util.Util; -import dev.loki.lovisual.config.i18n.ConfigValueAliasProvider; -import dev.loki.lovisual.config.definition.SettingDef; -import dev.loki.lovisual.config.values.mode.EnumValue; -import dev.loki.lovisual.config.values.primitive.NumberValue; -import dev.loki.lovisual.config.values.color.RGBAColorValue; -import dev.loki.lovisual.config.values.color.RGBColorValue; -import dev.loki.lovisual.features.gui.hud.registry.HudElementRegister; -import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; -import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; -import dev.loki.lovisual.features.module.core.Module; -import dev.loki.lovisual.features.module.core.ModuleManager; -import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors; -import dev.loki.lovisual.render.engine.math.HudScale; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.text.font.info.FontInfo; -import dev.loki.lovisual.render.engine.text.font.Fonts; -import dev.loki.lovisual.render.engine.text.render.TextRenderer; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; - -import static dev.loki.lovisual.features.theme.Theme.theme; - -@HudElementRegister(order = 10) -public final class ModuleList extends DraggableHudElement implements ConfigValueAliasProvider { - - private static final float BASE_LINE_H = 9f; - private static final float BASE_TEXT_Y = 3f; - private static final float BASE_TEXT_X = 3f; - private static final float BASE_TEXT_X_REV = 2f; - private static final float BASE_STRING_PAD = 3f; - private static final float BASE_RECT_EXTRA = 1f; - private static final float BASE_RADIUS = 2f; - private static final float BASE_SOFTNESS = 0.55f; - private final HudGlobalConfig hud = HudGlobalConfig.get(); - private final NumberValue scaleValue = - new NumberValue<>("module_list_scale", 3.67, 0.5, 5.0); - private final EnumValue colorMode = - new EnumValue<>("module_list_color_mode", ColorMode.CUSTOM, ColorMode.THEME, ColorMode.CUSTOM); - private final EnumValue bgEffect = - new EnumValue<>("module_list_bg_effect", BgEffect.BLUR, BgEffect.NONE, BgEffect.BLUR, BgEffect.GLASS); - private final NumberValue bgAlpha = - new NumberValue<>("module_list_bg_alpha", 226, 0, 255); - private final NumberValue blurAlpha = - new NumberValue<>("module_list_blur_alpha", 255, 0, 255); - private final RGBAColorValue bgTopColor = - new RGBAColorValue("module_list_bg", "#FF444242"); - private final RGBAColorValue bgBottomColor = - new RGBAColorValue("module_list_bg_secondary", "#22000000"); - private final RGBColorValue textTopColor = - new RGBColorValue("module_list_text", "RNB#E87171"); - private final RGBColorValue textBottomColor = - new RGBColorValue("module_list_text_secondary", "#93118C"); - private final EnumValue textGradientMode = - new EnumValue<>("module_list_text_gradient_mode", - AnimatedRenderColors.Mode.RAINBOW, - AnimatedRenderColors.Mode.STATIC, - AnimatedRenderColors.Mode.RAINBOW, - AnimatedRenderColors.Mode.LIGHT_RAINBOW, - AnimatedRenderColors.Mode.SKY, - AnimatedRenderColors.Mode.FADE, - AnimatedRenderColors.Mode.DOUBLE_COLOR, - AnimatedRenderColors.Mode.ANALOGOUS, - AnimatedRenderColors.Mode.THEME); - private final NumberValue textGradientSpeed = - new NumberValue<>("module_list_text_gradient_speed", 18, 2, 54); - private final EnumValue textEffect = - new EnumValue<>("module_list_text_effect", TextEffect.NONE, TextEffect.NONE, TextEffect.SHIMMER); - private final NumberValue textEffectSpeed = - new NumberValue<>("module_list_text_effect_speed", 30, 1, 60); - private final List rows = new ArrayList<>(); - private final List enabledModules = new ArrayList<>(); - private float renderX; - private float renderY; - private int cachedModulesHash; - private int cachedModulesCount; - private float cachedTextScale = -1f; - public ModuleList() { - super("module_list", "ModuleList", false); - } - - @Override - public List getLegacyValueNames(String currentValueName) { - if (currentValueName == null) { - return List.of(); - } - if (currentValueName.startsWith("module_list_")) { - return List.of("active_modules_" + currentValueName.substring("module_list_".length())); - } - return List.of(); - } - - @Override - protected void defineSettings(List defs) { - defs.add(SettingDef.number(scaleValue)); - defs.add(SettingDef.mode(colorMode)); - defs.add(SettingDef.mode(bgEffect)); - defs.add(SettingDef.number(bgAlpha).visibleWhen(this::isThemeMode)); - defs.add(SettingDef.number(blurAlpha).visibleWhen(this::hasEffect)); - defs.add(SettingDef.color(bgTopColor).visibleWhen(this::isCustomMode)); - defs.add(SettingDef.color(bgBottomColor).visibleWhen(this::isCustomMode)); - defs.add(SettingDef.colorNoAlpha(textTopColor).visibleWhen(this::isCustomMode)); - defs.add(SettingDef.colorNoAlpha(textBottomColor).visibleWhen(this::isCustomMode)); - defs.add(SettingDef.mode(textGradientMode)); - defs.add(SettingDef.number(textGradientSpeed).visibleWhen(this::hasAnimatedTextGradient)); - defs.add(SettingDef.mode(textEffect)); - defs.add(SettingDef.number(textEffectSpeed).visibleWhen(() -> textEffect.get() != TextEffect.NONE)); - } - - @Override - public void applyDefaultPosition(int screenW, int screenH) { - this.x = screenW - 80f; - this.y = 20f; - } - - @Override - public boolean usesEngineRenderer() { - return true; - } - - @Override - public void renderEngine(Renderer2D renderer, - TextRenderer textRenderer, - GuiGraphicsExtractor ctx, - float tickDelta, - int screenW, - int screenH) { - Minecraft mc = Minecraft.getInstance(); - if (!isEnabled() || mc == null || mc.player == null) { - rows.clear(); - width = 0f; - height = 0f; - return; - } - - TextRenderer listRenderer = Fonts.renderer("Comfortaa", FontInfo.Type.Regular, TextRenderer.get()); - if (listRenderer == null) { - listRenderer = textRenderer; - } - - float baseScale = HudScale.scale(screenW, screenH) * scaleValue.get().floatValue(); - float lineH = BASE_LINE_H * baseScale; - float textOffsetY = BASE_TEXT_Y * baseScale; - float textOffsetX = BASE_TEXT_X * baseScale; - float textOffsetXRev = BASE_TEXT_X_REV * baseScale; - float stringPad = BASE_STRING_PAD * baseScale; - float rectExtra = BASE_RECT_EXTRA * baseScale; - float radius = BASE_RADIUS * baseScale; - float softness = BASE_SOFTNESS * baseScale; - - float fontHeight = Math.max(1.0f, (float) listRenderer.getHeight(false)); - float targetTextHeight = Math.max(4.4f * baseScale, lineH - 4.2f * baseScale); - float textScale = Math.max(0.2f, targetTextHeight / fontHeight); - - enabledModules.clear(); - int modulesHash = 1; - for (Module module : ModuleManager.getModules()) { - if (module != null && module.isEnabled() && module.isShownInModuleList()) { - enabledModules.add(module); - String label = module.getDisplayName(); - modulesHash = 31 * modulesHash + System.identityHashCode(module); - modulesHash = 31 * modulesHash + (label != null ? label.hashCode() : 0); - } - } - - if (enabledModules.isEmpty()) { - rows.clear(); - width = 0f; - height = 0f; - return; - } - - boolean layoutChanged = modulesHash != cachedModulesHash - || enabledModules.size() != cachedModulesCount - || Math.abs(textScale - cachedTextScale) > 0.0001f; - - if (layoutChanged) { - rows.clear(); - listRenderer.begin(textScale, false, false); - for (Module module : enabledModules) { - Row row = new Row(module, module.getDisplayName()); - row.textWidth = (float) listRenderer.getWidth(row.label, false); - rows.add(row); - } - listRenderer.end(); - rows.sort(Comparator.comparingDouble((Row row) -> row.textWidth).reversed()); - cachedModulesHash = modulesHash; - cachedModulesCount = enabledModules.size(); - cachedTextScale = textScale; - } - - float maxWidth = 0f; - for (Row row : rows) { - maxWidth = Math.max(maxWidth, row.textWidth + stringPad + rectExtra); - } - - float maxX = Math.max(0f, screenW - maxWidth); - if (x > maxX) x = maxX; - if (x < 0f) x = 0f; - - boolean reverse = x > (screenW * 0.5f); - float reversedX = x; - float drawBaseX = reverse ? (reversedX + maxWidth) : x; - float listHeight = rows.size() * lineH; - - renderX = reverse ? (drawBaseX - maxWidth) : drawBaseX; - renderY = y; - width = maxWidth; - height = listHeight; - - int bgTop = resolveBgTop(); - int bgBottom = resolveBgBottom(); - int textTop = resolveTextTop(); - int textBottom = resolveTextBottom(); - boolean blur = isBlurEffect(); - boolean glass = isGlassEffect(); - boolean shimmer = textEffect.get() == TextEffect.SHIMMER; - float time = (float) (Util.getMillis() / 1000.0); - float blurQuality = hud != null ? hud.getBlurRadius() : 12.0f; - boolean autoBatch = !Renderer2D.isBatching(); - if (autoBatch) renderer.begin(); - try { - for (int i = 0; i < rows.size(); i++) { - Row row = rows.get(i); - float offsetY = y + i * lineH; - float stringWidth = row.textWidth + stringPad; - float rowX = reverse ? (drawBaseX - stringWidth) : drawBaseX; - float rowW = stringWidth + rectExtra; - - row.x = rowX; - row.y = offsetY; - row.w = rowW; - row.fillTop = rowFill(bgTop, i, rows.size()); - row.fillBottom = rowFill(bgBottom, i, rows.size()); - } - - if (blur) { - renderer.blurComposite(composite -> { - for (int i = 0; i < rows.size(); i++) { - Row row = rows.get(i); - int tint = HudRenderUtil.mixColor(row.fillTop, row.fillBottom, 0.5f) & 0x00FFFFFF; - composite.roundedRect(row.x, row.y, row.w, lineH, radius, blurQuality, 1.0f, blurAlpha.get() / 255f, tint); - } - }); - } else if (glass) { - for (int i = 0; i < rows.size(); i++) { - Row row = rows.get(i); - drawGlass(row.x, row.y, row.w, lineH, radius); - } - } - - for (int i = 0; i < rows.size(); i++) { - Row row = rows.get(i); - if (glass) { - int glassTop = HudRenderUtil.scaleAlpha(row.fillTop, 0.22f); - int glassBottom = HudRenderUtil.scaleAlpha(row.fillBottom, 0.18f); - renderer.roundedRectGradientQuad(row.x, row.y, row.w, lineH, radius, softness, - glassTop, glassTop, glassBottom, glassBottom); - } else { - int drawTop = blur ? HudRenderUtil.scaleAlpha(row.fillTop, 0.42f) : row.fillTop; - int drawBottom = blur ? HudRenderUtil.scaleAlpha(row.fillBottom, 0.42f) : row.fillBottom; - renderer.roundedRectGradientQuad(row.x, row.y, row.w, lineH, radius, softness, - drawTop, drawTop, drawBottom, drawBottom); - } - } - } finally { - if (autoBatch) renderer.render(); - } - - int gradientTop = resolveTextGradientTop(); - int gradientBottom = resolveTextGradientBottom(); - boolean topRainbow = isCustomMode() && textTopColor.isRainbow(); - boolean bottomRainbow = isCustomMode() && textBottomColor.isRainbow(); - AnimatedRenderColors.Mode gradientMode = textGradientMode.get(); - int gradientSpeed = hasAnimatedTextGradient() ? textGradientSpeed.get() : Math.max(1, textEffectSpeed.get()); - - listRenderer.begin(textScale, false, false); - float textHeight = (float) listRenderer.getHeight(false); - float blockTop = renderY; - float blockBottom = renderY + height; - for (Row row : rows) { - float textX = reverse ? (drawBaseX - (row.textWidth + stringPad) + textOffsetXRev) : (drawBaseX + textOffsetX); - float textY = row.y + textOffsetY + (lineH - textHeight - textOffsetY) * 0.08f; - listRenderer.renderQuadGradient(row.label, textX, textY, (idx, cp, x0, y0, x1, y1, out) -> - AnimatedRenderColors.moduleListBlockGlyphGradient( - gradientTop, - gradientBottom, - gradientMode, - gradientSpeed, - y0, - y1, - blockTop, - blockBottom, - idx, - time, - topRainbow, - bottomRainbow, - shimmer, - out - ), false); - } - listRenderer.end(); - } - - @Override - public boolean contains(float mx, float my) { - return mx >= renderX && mx <= renderX + width - && my >= renderY && my <= renderY + height; - } - - private int resolveBgTop() { - if (isThemeMode()) { - int base = HudRenderUtil.mixColor(theme().windowBg(), 0xFF000000, 0.38f); - return HudRenderUtil.setAlpha(base, bgAlpha.get()); - } - return bgTopColor.getArgb(); - } - - private int resolveBgBottom() { - if (isThemeMode()) { - int base = HudRenderUtil.mixColor(theme().windowHeader(), 0xFF000000, 0.48f); - return HudRenderUtil.setAlpha(base, bgAlpha.get()); - } - return bgBottomColor.getArgb(); - } - - private int resolveTextTop() { - if (isThemeMode()) { - return HudRenderUtil.mixColor(theme().accentSoft(), theme().accent(), 0.18f); - } - return textTopColor.getArgb(); - } - - private int resolveTextBottom() { - if (isThemeMode()) { - return theme().accent(); - } - return textBottomColor.getArgb(); - } - - private int resolveTextGradientTop() { - if (isThemeMode()) { - return resolveTextTop(); - } - return textTopColor.isRainbow() ? parseRgbFallback(textTopColor.rainbowFallbackHex()) : textTopColor.getArgb(); - } - - private int resolveTextGradientBottom() { - if (isThemeMode()) { - return resolveTextBottom(); - } - return textBottomColor.isRainbow() ? parseRgbFallback(textBottomColor.rainbowFallbackHex()) : textBottomColor.getArgb(); - } - - private int parseRgbFallback(String hex) { - if (hex == null || hex.isBlank()) return 0xFFFFFFFF; - String s = hex.trim(); - if (s.startsWith("#")) s = s.substring(1); - if (s.startsWith("0x") || s.startsWith("0X")) s = s.substring(2); - try { - int value = (int) Long.parseUnsignedLong(s, 16); - if (s.length() <= 6) return 0xFF000000 | (value & 0x00FFFFFF); - return value; - } catch (NumberFormatException ignored) { - return 0xFFFFFFFF; - } - } - - private int rowFill(int argb, int index, int total) { - if (!isThemeMode() || total <= 1) { - return argb; - } - float progress = (float) index / (float) (total - 1); - int alpha = (argb >>> 24) & 0xFF; - int rgb = HudRenderUtil.mixColor(argb, resolveTextBottom(), 0.03f + 0.07f * progress); - return (rgb & 0x00FFFFFF) | (alpha << 24); - } - - private void drawBlur(float x, float y, float w, float h, float radius, int tintArgb, float quality) { - float alpha = blurAlpha.get() / 255f; - Renderer2D.COLOR.blurRect(x, y, w, h, radius, quality, 1.0f, alpha, 0xFFFFFF); - } - - private void drawGlass(float x, float y, float w, float h, float radius) { - float blurStrength = blurAlpha.get() / 255f; - if (blurStrength <= 0.001f) return; - Renderer2D.COLOR.liquidGlassRect( - x, y, w, h, radius, - 0xFFFFFFFF, - 1.0f, - blurStrength, - Renderer2D.LiquidGlassPreset.BALANCED - ); - } - - private boolean isThemeMode() { - return colorMode.get() == ColorMode.THEME; - } - - private boolean isCustomMode() { - return colorMode.get() == ColorMode.CUSTOM; - } - - private boolean isBlurEffect() { - return bgEffect.get() == BgEffect.BLUR; - } - - private boolean isGlassEffect() { - return bgEffect.get() == BgEffect.GLASS; - } - - private boolean hasEffect() { - return bgEffect.get() != BgEffect.NONE; - } - - private boolean hasAnimatedTextGradient() { - return textGradientMode.get() != AnimatedRenderColors.Mode.STATIC - || (isCustomMode() && (textTopColor.isRainbow() || textBottomColor.isRainbow())); - } - - private enum ColorMode implements EnumValue.IdProvider { - THEME("Theme"), - CUSTOM("Custom"); - - private final String id; - - ColorMode(String id) { - this.id = id; - } - - @Override - public String id() { - return id; - } - } - - private enum BgEffect implements EnumValue.IdProvider { - NONE("None"), - BLUR("Blur"), - GLASS("Glass"); - - private final String id; - - BgEffect(String id) { - this.id = id; - } - - @Override - public String id() { - return id; - } - } - - private enum TextEffect implements EnumValue.IdProvider { - NONE("None"), - SHIMMER("Shimmer"); - - private final String id; - - TextEffect(String id) { - this.id = id; - } - - @Override - public String id() { - return id; - } - } - - private static final class Row { - private final Module module; - private final String label; - private float textWidth; - private float x; - private float y; - private float w; - private int fillTop; - private int fillBottom; - - private Row(Module module, String label) { - this.module = module; - this.label = label; - } - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/Radar.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/Radar.java deleted file mode 100644 index 1ced787..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/Radar.java +++ /dev/null @@ -1,392 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.features.gui.hud.draggable.impl.panels; - -import dev.loki.lovisual.config.values.color.RGBAColorValue; -import dev.loki.lovisual.config.values.color.RGBColorValue; -import dev.loki.lovisual.config.values.mode.EnumValue; -import dev.loki.lovisual.config.values.primitive.BooleanValue; -import dev.loki.lovisual.config.values.primitive.NumberValue; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.client.multiplayer.PlayerInfo; -import net.minecraft.util.Mth; -import net.minecraft.world.entity.player.Player; -import dev.loki.lovisual.config.definition.SettingDef; -import dev.loki.lovisual.features.gui.hud.registry.HudElementRegister; -import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; -import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; -import dev.loki.lovisual.features.relations.category.CategoryService; -import dev.loki.lovisual.render.engine.color.RenderColor; -import dev.loki.lovisual.render.engine.math.HudScale; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.text.font.info.FontInfo; -import dev.loki.lovisual.render.engine.text.font.Fonts; -import dev.loki.lovisual.render.engine.text.render.TextRenderer; -import dev.loki.lovisual.util.player.health.PlayerHealthResolver; - -import java.util.List; - -import static dev.loki.lovisual.features.theme.Theme.theme; -import dev.loki.lovisual.features.gui.hud.draggable.impl.stats.Ping; - -/** - * Player radar widget with a rectangular map and text list. - */ -@HudElementRegister(order = 140) -public final class Radar extends DraggableHudElement { - - { - defaultLayout(1666.0f, 17.915222f); - } - - private static final float BASE_RADIUS = 6f; - private static final float BASE_LINE_W = 1.0f; - private static final float BASE_PAD = 3f; - private static final float BLIP_SIZE = 4f; - private static final float BLIP_MARGIN = 6f; - private static final float TEXT_SCALE_MULT = 0.6f; - private static final String FIELD_KEY_PREFIX = "radar_field_"; - private static final Field[] FIELD_OPTIONS = { - Field.NAME, - Field.HP, - Field.DISTANCE, - Field.PING, - Field.NONE - }; - private final HudGlobalConfig hud = HudGlobalConfig.get(); - private final EnumValue mode = - new EnumValue<>("radar_mode", Mode.RECT, Mode.RECT, Mode.TEXT); - private final EnumValue colorMode = - new EnumValue<>("radar_color_mode", ColorMode.SYNC, ColorMode.SYNC, ColorMode.CUSTOM); - private final NumberValue size = - new NumberValue<>("radar_size", 217.27, 20.0, 300.0); - private final RGBAColorValue bgColor = - new RGBAColorValue("radar_bg_color", "#CC101010"); - private final NumberValue bgAlpha = - new NumberValue<>("radar_bg_alpha", 109, 0, 255); - private final RGBColorValue textColor = - new RGBColorValue("radar_text_color", "#FFFFFF"); - private final EnumValue field1 = fieldSlot(1, Field.NAME); - private final EnumValue field2 = fieldSlot(2, Field.HP); - private final EnumValue field3 = fieldSlot(3, Field.PING); - private final EnumValue field4 = fieldSlot(4, Field.NONE); - private final EnumValue field5 = fieldSlot(5, Field.NONE); - private final BooleanValue blur = - new BooleanValue("radar_blur", true); - private final NumberValue blurAlpha = - new NumberValue<>("radar_blur_alpha", 255, 0, 255); - private final BooleanValue gradient = - new BooleanValue("radar_gradient", false); - public Radar() { - super("radar", "Radar", false); - } - - private static EnumValue fieldSlot(int slot, Field def) { - return new EnumValue<>(FIELD_KEY_PREFIX + slot, def, FIELD_OPTIONS); - } - - @Override - protected void defineSettings(List defs) { - defs.add(SettingDef.mode(mode)); - defs.add(SettingDef.mode(colorMode)); - defs.add(SettingDef.number(size)); - defs.add(SettingDef.color(bgColor) - .visibleWhen(() -> colorMode.get() == ColorMode.CUSTOM)); - defs.add(SettingDef.number(bgAlpha) - .visibleWhen(() -> colorMode.get() == ColorMode.SYNC)); - defs.add(SettingDef.colorNoAlpha(textColor) - .visibleWhen(() -> mode.get() == Mode.TEXT - && colorMode.get() == ColorMode.CUSTOM)); - defs.add(SettingDef.mode(field1) - .visibleWhen(() -> mode.get() == Mode.TEXT)); - defs.add(SettingDef.mode(field2) - .visibleWhen(() -> mode.get() == Mode.TEXT)); - defs.add(SettingDef.mode(field3) - .visibleWhen(() -> mode.get() == Mode.TEXT)); - defs.add(SettingDef.mode(field4) - .visibleWhen(() -> mode.get() == Mode.TEXT)); - defs.add(SettingDef.mode(field5) - .visibleWhen(() -> mode.get() == Mode.TEXT)); - defs.add(SettingDef.bool(blur)); - defs.add(SettingDef.number(blurAlpha).visibleWhen(blur::get)); - defs.add(SettingDef.bool(gradient) - .visibleWhen(() -> colorMode.get() == ColorMode.SYNC)); - } - - @Override - public void applyDefaultPosition(int screenW, int screenH) { - this.x = 120f; - this.y = 120f; - } - - @Override - public boolean usesEngineRenderer() { - return true; - } - - @Override - public void renderEngine(Renderer2D renderer, - TextRenderer textRenderer, - GuiGraphicsExtractor ctx, - float tickDelta, - int screenW, - int screenH) { - Minecraft mc = Minecraft.getInstance(); - boolean preview = DraggableHudElementRegistry.isForceVisible(); - if (mc == null && !preview) { - width = 0f; - height = 0f; - return; - } - if (!preview && !isEnabled()) { - width = 0f; - height = 0f; - return; - } - - if (mode.get() == Mode.TEXT) { - renderTextMode(textRenderer, screenW, screenH, preview); - } else { - renderRectMode(renderer, tickDelta, screenW, screenH); - } - } - - private void renderRectMode(Renderer2D renderer, float tickDelta, int screenW, int screenH) { - float scale = HudScale.scale(screenW, screenH) - * (hud.getFontSize() / 18f); - float size = this.size.get().floatValue() * scale; - float baseX = x; - float baseY = y; - float radius = Math.min(BASE_RADIUS * scale, size * 0.25f); - float lineW = Math.max(0.5f, BASE_LINE_W * scale); - float pad = BASE_PAD * scale; - - int bgAlpha = this.bgAlpha.get(); - boolean syncTheme = colorMode.get() == ColorMode.SYNC; - int bgRgb = syncTheme - ? (theme().windowBg() & 0x00FFFFFF) - : (bgColor.getArgb() & 0x00FFFFFF); - int bg = (bgAlpha << 24) | bgRgb; - - boolean blurEnabled = blur.get(); - if (blurEnabled) { - drawBlur(baseX, baseY, size, size, radius, bg); - } - boolean useGradient = syncTheme && gradient.get(); - HudRenderUtil.drawHudBackground(renderer, baseX, baseY, size, size, radius, 1.0f, bg, useGradient); - - int stroke = HudRenderUtil.setAlpha(theme().accent(), 0x55); - renderer.roundedRectStroke(baseX, baseY, size, size, radius, 1.0f, - Math.max(1.0f, 1.25f * scale), stroke); - - int cross = HudRenderUtil.setAlpha(theme().textMuted(), 0x55); - float cx = baseX + size * 0.5f; - float cy = baseY + size * 0.5f; - renderer.quad(cx - lineW * 0.5f, baseY + pad, lineW, size - pad * 2f, cross); - renderer.quad(baseX + pad, cy - lineW * 0.5f, size - pad * 2f, lineW, cross); - - renderBlips(renderer, tickDelta, cx, cy, size * 0.5f - BLIP_MARGIN * scale, scale); - - width = size; - height = size; - } - - private void renderBlips(Renderer2D renderer, float tickDelta, float cx, float cy, float half, float scale) { - Minecraft mc = Minecraft.getInstance(); - if (mc == null || mc.player == null || mc.level == null) return; - - float blip = Math.max(2.5f, BLIP_SIZE * scale); - float blipRadius = Math.max(1.0f, blip * 0.5f); - float yaw = mc.player.getViewYRot(tickDelta); - float yawRad = (float) Math.toRadians(yaw); - float cos = Mth.cos(yawRad); - float sin = Mth.sin(yawRad); - - for (Player player : mc.level.players()) { - if (player == mc.player) continue; - - double px = player.getX(); - double pz = player.getZ(); - double dx = px - mc.player.getX(); - double dz = pz - mc.player.getZ(); - - float rotY = (float) (-(dz * cos - dx * sin)) * 2.0f; - float rotX = (float) (-(dx * cos + dz * sin)) * 2.0f; - - rotX = Mth.clamp(rotX, -half, half); - rotY = Mth.clamp(rotY, -half, half); - - int color = CategoryService.getColor(player); - renderer.roundedRect(cx + rotX - blip * 0.5f, cy + rotY - blip * 0.5f, - blip, blip, blipRadius, 1.0f, color); - } - } - - private void renderTextMode(TextRenderer textRenderer, int screenW, int screenH, boolean preview) { - Minecraft mc = Minecraft.getInstance(); - boolean ready = mc != null && mc.player != null && mc.level != null; - if (!ready && !preview) { - width = 0f; - height = 0f; - return; - } - - TextRenderer fallback = textRenderer != null ? textRenderer : TextRenderer.get(); - TextRenderer renderer = Fonts.renderer("Iosevka", FontInfo.Type.Regular, fallback); - - float scale = HudScale.scale(screenW, screenH) - * (hud.getFontSize() / 18f) - * TEXT_SCALE_MULT; - - renderer.begin(scale, false, false); - float lineH = (float) renderer.getHeight(false); - renderer.end(); - - int textColor = colorMode.get() == ColorMode.CUSTOM - ? 0xFF000000 | (this.textColor.getArgb() & 0x00FFFFFF) - : theme().accent(); - - float maxW = 0f; - float yOff = 0f; - if (ready) { - for (Player player : mc.level.players()) { - if (player == mc.player) continue; - String line = buildLine(player); - if (line.isEmpty()) continue; - - renderer.begin(scale, false, false); - float w = (float) renderer.getWidth(line, false); - renderer.render(line, x, y + yOff, new RenderColor(textColor), false); - renderer.end(); - - maxW = Math.max(maxW, w); - yOff += lineH; - } - } - - if (preview && yOff <= 0f) { - String line = "Player 20 40ms"; - renderer.begin(scale, false, false); - float w = (float) renderer.getWidth(line, false); - renderer.render(line, x, y, new RenderColor(textColor), false); - renderer.end(); - maxW = w; - yOff = lineH; - } - - width = maxW; - height = Math.max(lineH, yOff); - } - - private String buildLine(Player player) { - StringBuilder out = new StringBuilder(64); - appendField(out, field1.get(), player); - appendField(out, field2.get(), player); - appendField(out, field3.get(), player); - appendField(out, field4.get(), player); - appendField(out, field5.get(), player); - return out.toString().trim(); - } - - private void appendField(StringBuilder out, Field field, Player player) { - Minecraft mc = Minecraft.getInstance(); - if (field == null || field == Field.NONE) return; - if (out.length() > 0) out.append(' '); - switch (field) { - case HP -> { - int hp = (int) Math.ceil(PlayerHealthResolver.totalHealth(player)); - out.append(hp); - } - case DISTANCE -> { - int dist = (int) Math.ceil(mc.player.distanceTo(player)); - out.append(dist).append('m'); - } - case PING -> { - int ping = getPlayerPing(player); - if (ping >= 0) { - out.append(ping).append("ms"); - } else { - out.append("--"); - } - } - case NAME -> out.append(player.getName().getString()); - default -> { - // ignore - } - } - } - - private int getPlayerPing(Player player) { - Minecraft mc = Minecraft.getInstance(); - if (mc == null || mc.getConnection() == null) return -1; - PlayerInfo entry = mc.getConnection().getPlayerInfo(player.getUUID()); - return entry != null ? entry.getLatency() : -1; - } - - private void drawBlur(float x, float y, float w, float h, float radius, int tintRgb) { - float quality = hud.getBlurRadius(); - float brightness = 1.0f; - float alpha = blurAlpha.get() / 255f; - Renderer2D.COLOR.blurRect(x, y, w, h, radius, quality, brightness, alpha, 0xFFFFFF); - } - - private enum Mode implements EnumValue.IdProvider { - RECT("Rect"), - TEXT("Text"); - - private final String id; - - Mode(String id) { - this.id = id; - } - - @Override - public String id() { - return id; - } - } - - private enum ColorMode implements EnumValue.IdProvider { - SYNC("Sync"), - CUSTOM("Custom"); - - private final String id; - - ColorMode(String id) { - this.id = id; - } - - @Override - public String id() { - return id; - } - } - - private enum Field implements EnumValue.IdProvider { - NAME("Name"), - HP("Hp"), - DISTANCE("Distance"), - PING("Ping"), - NONE("None"); - - private final String id; - - Field(String id) { - this.id = id; - } - - @Override - public String id() { - return id; - } - } -} - diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/Scoreboard.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/Scoreboard.java deleted file mode 100644 index 35583c8..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/Scoreboard.java +++ /dev/null @@ -1,931 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.features.gui.hud.draggable.impl.panels; - -import dev.loki.lovisual.config.values.color.KeyBindValue; -import dev.loki.lovisual.config.values.color.RGBAColorValue; -import dev.loki.lovisual.config.values.color.RGBColorValue; -import dev.loki.lovisual.config.values.mode.BindMode; -import dev.loki.lovisual.config.values.mode.EnumValue; -import dev.loki.lovisual.config.values.mode.ModeValue; -import dev.loki.lovisual.config.values.primitive.BooleanMapValue; -import dev.loki.lovisual.config.values.primitive.NumberValue; -import dev.loki.lovisual.features.module.lifecycle.Modules; -import dev.loki.lovisual.util.screen.ClientScreen; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.MutableComponent; -import net.minecraft.network.chat.numbers.NumberFormat; -import net.minecraft.network.chat.numbers.StyledFormat; -import net.minecraft.world.scores.DisplaySlot; -import net.minecraft.world.scores.Objective; -import net.minecraft.world.scores.PlayerScoreEntry; -import net.minecraft.world.scores.PlayerTeam; -import dev.loki.lovisual.config.definition.SettingDef; -import dev.loki.lovisual.features.gui.hud.registry.HudElementRegister; -import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; -import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; -import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; -import dev.loki.lovisual.features.module.phase.HudPhase; -import dev.loki.lovisual.features.module.modules.visuals.visibility.NoRender; -import dev.loki.lovisual.render.engine.animation.AnimationUtility; -import dev.loki.lovisual.render.engine.color.RenderColor; -import dev.loki.lovisual.render.engine.math.HudScale; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.svg.model.SvgRenderOptions; -import dev.loki.lovisual.render.engine.text.font.info.FontInfo; -import dev.loki.lovisual.render.engine.text.font.Fonts; -import dev.loki.lovisual.render.engine.text.glyph.custom.TextGlyphFallback; -import dev.loki.lovisual.render.engine.text.render.TextRenderer; -import dev.loki.lovisual.util.input.KeyManager; -import dev.loki.lovisual.util.logging.ServerDumpUtil; -import dev.loki.lovisual.util.text.format.LegacyTextUtil; -import dev.loki.lovisual.util.text.format.TextRenderUtil; -import dev.loki.lovisual.util.text.format.TextRenderUtil.Part; - -import java.util.*; - -import static dev.loki.lovisual.features.theme.Theme.theme; - -@HudElementRegister(order = 110) -public final class Scoreboard extends DraggableHudElement { - - { - defaultLayout(1721.69f, 158.0f); - } - - private static final Comparator ENTRY_COMPARATOR = Comparator.comparingInt(PlayerScoreEntry::value) - .reversed() - .thenComparing(PlayerScoreEntry::owner, String.CASE_INSENSITIVE_ORDER); - - private static final float BASE_PAD_X = 8.0f; - private static final float BASE_PAD_Y = 8.0f; - private static final float BASE_ROW_GAP = 2.0f; - private static final float BASE_HEADER_GAP = 8.0f; - private static final float BASE_SCORE_GAP = 8.0f; - private static final float BASE_FOOTER_GAP = 8.0f; - private static final float BASE_RADIUS = 4.0f; - private static final float BASE_SOFTNESS = 1.0f; - private static final float BASE_STROKE = 0.45f; - private static final float BASE_TITLE_SCALE = 0.78f; - private static final float BASE_TEXT_SCALE = 0.68f; - private static final float BASE_TITLE_LINE_GAP = 1.0f; - private static final int MAX_ENTRIES = 15; - private static final String COLOR_THEME = "Theme"; - private static final String COLOR_CUSTOM = "Custom"; - private static final String EFFECT_NONE = "None"; - private static final String EFFECT_BLUR = "Blur"; - - private final HudGlobalConfig hud = HudGlobalConfig.get(); - - private final KeyBindValue toggleBind = - new KeyBindValue("toggle_bind", "Э"); - private final NumberValue scale = - new NumberValue<>("scoreboard_scale", 1.93, 0.5, 5.0); - private final ModeValue colorMode = - new ModeValue("scoreboard_color_mode", "Theme", COLOR_THEME, COLOR_CUSTOM); - private final ModeValue bgEffect = - new ModeValue("scoreboard_bg_effect", "Blur", EFFECT_NONE, EFFECT_BLUR); - private final NumberValue blurAlpha = - new NumberValue<>("scoreboard_blur_alpha", 255, 0, 255); - private final NumberValue bgAlpha = - new NumberValue<>("scoreboard_bg_alpha", 182, 0, 255); - private final RGBAColorValue bg = - new RGBAColorValue("scoreboard_bg", "#820A0A0A"); - private final RGBAColorValue bg2 = - new RGBAColorValue("scoreboard_bg_secondary", "#B8141414"); - private final RGBColorValue stroke = - new RGBColorValue("scoreboard_stroke", "#4A4A4A"); - private final RGBColorValue titleColor = - new RGBColorValue("scoreboard_title_color", "#FFCC00"); - private final RGBColorValue labelColor = - new RGBColorValue("scoreboard_label_color", "#FFFFFF"); - private final RGBColorValue valueColor = - new RGBColorValue("scoreboard_value_color", "#FF4A4A"); - private final RGBColorValue footerColor = - new RGBColorValue("scoreboard_footer_color", "#FFD44A"); - private final EnumValue textEffect = - new EnumValue<>("scoreboard_text_effect", HudTextEffects.Effect.STRIPE, - HudTextEffects.Effect.NONE, HudTextEffects.Effect.MIX, HudTextEffects.Effect.FLOW, - HudTextEffects.Effect.PULSE, HudTextEffects.Effect.STRIPE); - private final NumberValue textEffectSpeed = - new NumberValue<>("scoreboard_text_effect_speed", 18, 1, 60); - private final BooleanMapValue textEffectSections = - new BooleanMapValue("scoreboard_text_effect_sections", defaultTextEffectSections()); - private final List previewLinesSample = List.of( - new SidebarLine(Component.literal("Entry One"), Component.literal("12"), false), - new SidebarLine(Component.literal("Entry Two"), Component.literal("34"), false), - new SidebarLine(Component.literal("Entry Three"), Component.literal("56"), false), - new SidebarLine(Component.literal("Entry Four"), Component.literal("78"), false), - new SidebarLine(Component.literal("Entry Five"), Component.literal("90"), false), - new SidebarLine(Component.literal("Entry Six"), Component.literal("12%"), false), - new SidebarLine(Component.literal("Entry Seven"), Component.literal("3"), false), - new SidebarLine(Component.literal("Footer Text"), Component.empty(), true) - ); - - private final List lines = new ArrayList<>(); - - private boolean toggleArmed = true; - private boolean toggleInit = false; - private int uiBgPrimary; - private int uiBgSecondary; - private int uiHeaderBg; - private int uiStroke; - private int uiTitleColor; - private int uiLabelColor; - private int uiValueColor; - private int uiFooterColor; - private float displayLabelWidth = -1.0f; - private float displayValueWidth = -1.0f; - private float displayFooterWidth = -1.0f; - - public Scoreboard() { - super("scoreboard", "Scoreboard", false); - } - - public static boolean shouldReplaceVanilla() { - return DraggableHudElementRegistry.isEnabled(Scoreboard.class) && !isHiddenByNoRender(); - } - - private static float smoothWidth(float current, float target) { - if (current < 0.0f) return target; - if (target >= current) return target; - float next = AnimationUtility.approach(current, target, AnimationUtility.deltaTime(), 12.0f); - return AnimationUtility.snap(next, target, 0.25f); - } - - private static boolean isHiddenByNoRender() { - NoRender noRender = Modules.get(NoRender.class); - return noRender != null && noRender.hideScoreboard(); - } - - private static Map defaultTextEffectSections() { - Map sections = new LinkedHashMap<>(); - sections.put("header", true); - sections.put("labels", false); - sections.put("values", false); - sections.put("footer", false); - return sections; - } - - private static float measureStyledText(TextRenderer fallback, Component text, int defaultColor, float scale) { - if (fallback == null || text == null) return 0.0f; - float width = 0.0f; - for (Part part : TextRenderUtil.flattenStyled(text, defaultColor)) { - width += partWidth(fallback, part, scale, 0.0f, 0); - } - return width; - } - - private static List> splitStyledLines(Component text, int defaultColor) { - List> lines = new ArrayList<>(); - List current = new ArrayList<>(); - lines.add(current); - for (Part part : TextRenderUtil.flattenStyled(text, defaultColor)) { - String value = part.text(); - int start = 0; - while (true) { - int newline = value.indexOf('\n', start); - String segment = newline >= 0 ? value.substring(start, newline) : value.substring(start); - if (!segment.isEmpty()) { - current.add(new Part( - segment, - part.color(), - part.bold(), - part.italic(), - part.underline(), - part.strikethrough(), - part.obfuscated() - )); - } - if (newline < 0) { - break; - } - current = new ArrayList<>(); - lines.add(current); - start = newline + 1; - } - } - return lines; - } - - private static float measureStyledLineWidth(TextRenderer fallback, List> lines, float scale) { - if (fallback == null || lines == null || lines.isEmpty()) return 0.0f; - float widest = 0.0f; - for (List line : lines) { - float width = 0.0f; - int partIndex = 0; - for (Part part : line) { - width += partWidth(fallback, part, scale, 0.0f, partIndex++); - } - widest = Math.max(widest, width); - } - return widest; - } - - private static void renderStyledText(Renderer2D shapeRenderer, - TextRenderer fallback, - float scale, - float x, - float y, - Component text, - int defaultColor, - boolean animated, - HudTextEffects.Effect effect, - int effectSpeed, - float timeSec, - float phase) { - if (fallback == null || text == null) return; - float cursorX = x; - int partIndex = 0; - float lineHeight = rendererHeight(fallback, scale); - for (Part part : TextRenderUtil.flattenStyled(text, defaultColor)) { - TextRenderer renderer = styledRenderer(fallback, part); - String renderText = part.obfuscated() ? obfuscate(part.text(), timeSec + phase, partIndex) : part.text(); - float partW = renderTextRuns(shapeRenderer, renderer, scale, cursorX, y, renderText, part.color(), animated, - effect, effectSpeed, timeSec + phase + partIndex * 0.12f); - renderDecorations(shapeRenderer, cursorX, y, partW, lineHeight, part); - cursorX += partW; - partIndex++; - } - } - - private static void renderStyledLinesCentered(Renderer2D shapeRenderer, - TextRenderer fallback, - float scale, - float boxX, - float boxWidth, - float y, - List> lines, - float lineGap, - boolean animated, - HudTextEffects.Effect effect, - int effectSpeed, - float timeSec) { - if (fallback == null || lines == null || lines.isEmpty()) return; - float lineHeight = rendererHeight(fallback, scale); - float cursorY = y; - for (int lineIndex = 0; lineIndex < lines.size(); lineIndex++) { - List line = lines.get(lineIndex); - float lineWidth = 0.0f; - int measurePart = 0; - for (Part part : line) { - lineWidth += partWidth(fallback, part, scale, timeSec + lineIndex * 0.18f, measurePart++); - } - float cursorX = boxX + Math.max(0.0f, (boxWidth - lineWidth) * 0.5f); - int partIndex = 0; - for (Part part : line) { - TextRenderer renderer = styledRenderer(fallback, part); - String renderText = part.obfuscated() ? obfuscate(part.text(), timeSec + lineIndex * 0.18f, partIndex) : part.text(); - float partW = renderTextRuns(shapeRenderer, renderer, scale, cursorX, cursorY, renderText, part.color(), animated, - effect, effectSpeed, timeSec + lineIndex * 0.18f + partIndex * 0.12f); - renderDecorations(shapeRenderer, cursorX, cursorY, partW, lineHeight, part); - cursorX += partW; - partIndex++; - } - cursorY += lineHeight + lineGap; - } - } - - private static TextRenderer styledRenderer(TextRenderer fallback, Part part) { - if (part == null) return fallback; - FontInfo.Type type; - if (part.bold() && part.italic()) { - type = FontInfo.Type.BoldItalic; - } else if (part.bold()) { - type = FontInfo.Type.Bold; - } else if (part.italic()) { - type = FontInfo.Type.Italic; - } else { - type = FontInfo.Type.Regular; - } - TextRenderer renderer = Fonts.renderer("Iosevka", type, fallback); - return renderer != null ? renderer : fallback; - } - - private static TextRenderer rendererForGlyph(TextRenderer preferred, int codePoint) { - return TextGlyphFallback.rendererForGlyph(preferred, codePoint); - } - - private static float rendererHeight(TextRenderer renderer, float scale) { - if (renderer == null) return 0.0f; - renderer.begin(scale, false, false); - float height = (float) renderer.getHeight(false); - renderer.end(); - return height; - } - - private static float partWidth(TextRenderer fallback, Part part, float scale, float timeSec, int salt) { - if (fallback == null || part == null || part.text().isEmpty()) return 0.0f; - TextRenderer renderer = styledRenderer(fallback, part); - String text = part.obfuscated() ? obfuscate(part.text(), timeSec, salt) : part.text(); - return measureTextRuns(renderer, scale, text); - } - - private static float measureTextRuns(TextRenderer preferred, float scale, String text) { - if (preferred == null || text == null || text.isEmpty()) return 0.0f; - TextRenderer current = null; - float width = 0.0f; - float svgAdvance = svgGlyphSize(preferred, scale); - try { - for (int i = 0; i < text.length(); ) { - int cp = text.codePointAt(i); - if (TextGlyphFallback.shouldUseVanillaSvg(preferred, cp)) { - if (current != null) { - current.end(); - current = null; - } - width += svgAdvance; - i += Character.charCount(cp); - continue; - } - String glyph = new String(Character.toChars(cp)); - TextRenderer next = rendererForGlyph(preferred, cp); - if (next != current) { - if (current != null) current.end(); - current = next; - current.begin(scale, false, false); - } - width += (float) current.getWidth(glyph, false); - i += Character.charCount(cp); - } - return width; - } finally { - if (current != null) current.end(); - } - } - - private static float renderTextRuns(Renderer2D shapeRenderer, - TextRenderer preferred, - float scale, - float x, - float y, - String text, - int color, - boolean animated, - HudTextEffects.Effect effect, - int effectSpeed, - float timeSec) { - if (preferred == null || text == null || text.isEmpty()) return 0.0f; - TextRenderer current = null; - StringBuilder run = new StringBuilder(); - float cursorX = x; - float runX = x; - int glyphIndex = 0; - float svgSize = svgGlyphSize(preferred, scale); - float svgY = y + (rendererHeight(preferred, scale) - svgSize) * 0.5f; - for (int i = 0; i < text.length(); ) { - int cp = text.codePointAt(i); - if (TextGlyphFallback.shouldUseVanillaSvg(preferred, cp)) { - if (current != null && !run.isEmpty()) { - cursorX = flushTextRun(current, run.toString(), scale, runX, y, color, animated, - effect, effectSpeed, timeSec + glyphIndex * 0.12f); - run.setLength(0); - } - current = null; - String svgName = TextGlyphFallback.vanillaSvgName(cp); - if (shapeRenderer != null && svgName != null) { - shapeRenderer.svg(svgName, cursorX, svgY, svgSize, svgSize, - SvgRenderOptions.fromFile().withAlpha(((color >>> 24) & 0xFF) / 255.0f)); - } - cursorX += svgSize; - runX = cursorX; - i += Character.charCount(cp); - glyphIndex++; - continue; - } - TextRenderer next = rendererForGlyph(preferred, cp); - if (current != null && next != current) { - cursorX = flushTextRun(current, run.toString(), scale, runX, y, color, animated, - effect, effectSpeed, timeSec + glyphIndex * 0.12f); - run.setLength(0); - runX = cursorX; - } - if (next != current) { - current = next; - } - run.appendCodePoint(cp); - i += Character.charCount(cp); - glyphIndex++; - } - if (current != null && !run.isEmpty()) { - cursorX = flushTextRun(current, run.toString(), scale, runX, y, color, animated, - effect, effectSpeed, timeSec + glyphIndex * 0.12f); - } - return cursorX - x; - } - - private static float svgGlyphSize(TextRenderer renderer, float scale) { - return Math.max(1.0f, rendererHeight(renderer, scale)) * 0.92f; - } - - private static float flushTextRun(TextRenderer renderer, - String text, - float scale, - float x, - float y, - int color, - boolean animated, - HudTextEffects.Effect effect, - int effectSpeed, - float timeSec) { - if (renderer == null || text == null || text.isEmpty()) return x; - renderer.begin(scale, false, false); - boolean rendered = animated && HudTextEffects.render( - renderer, - text, - x, - y, - color, - effect, - effectSpeed, - timeSec, - false - ); - if (!rendered) { - renderer.render(text, x, y, new RenderColor(color), false); - } - float width = (float) renderer.getWidth(text, false); - renderer.end(); - return x + width; - } - - private static void renderDecorations(Renderer2D renderer, - float x, - float y, - float w, - float lineHeight, - Part part) { - if (renderer == null || part == null || w <= 0.0f) return; - if (!part.underline() && !part.strikethrough()) return; - int color = part.color(); - float h = Math.max(0.65f, lineHeight * 0.055f); - if (part.underline()) { - renderer.quad(x, y + lineHeight * 0.88f, w, h, color); - } - if (part.strikethrough()) { - renderer.quad(x, y + lineHeight * 0.54f, w, h, color); - } - } - - private static String obfuscate(String text, float timeSec, int salt) { - if (text == null || text.isEmpty()) return ""; - final String alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - int seed = Math.abs((int) (timeSec * 18.0f) * 31 + salt * 131); - StringBuilder out = new StringBuilder(text.length()); - for (int i = 0; i < text.length(); i++) { - char ch = text.charAt(i); - if (Character.isWhitespace(ch)) { - out.append(ch); - } else { - out.append(alphabet.charAt(Math.floorMod(seed + i * 17 + ch, alphabet.length()))); - } - } - return out.toString(); - } - - private static boolean isBlankText(Component text) { - return text == null || text.getString().isBlank(); - } - - private static Component normalizeHeaderTitle(Component text) { - if (text == null) { - return Component.empty(); - } - MutableComponent out = Component.empty(); - text.visit((style, value) -> { - if (value != null && !value.isEmpty()) { - out.append(Component.literal(normalizeHeaderGlyphs(value)).setStyle(style)); - } - return java.util.Optional.empty(); - }, net.minecraft.network.chat.Style.EMPTY); - return out; - } - - private static String normalizeHeaderGlyphs(String input) { - if (input == null || input.isEmpty()) { - return ""; - } - StringBuilder out = new StringBuilder(input.length()); - for (int i = 0; i < input.length(); i++) { - char ch = input.charAt(i); - out.append(switch (ch) { - case 'ᴀ' -> 'a'; - case 'ʙ' -> 'b'; - case 'ᴄ' -> 'c'; - case 'ᴅ' -> 'd'; - case 'ᴇ' -> 'e'; - case 'ꜰ' -> 'f'; - case 'ɢ' -> 'g'; - case 'ʜ' -> 'h'; - case 'ɪ' -> 'i'; - case 'ᴊ' -> 'j'; - case 'ᴋ' -> 'k'; - case 'ʟ' -> 'l'; - case 'ᴍ' -> 'm'; - case 'ɴ' -> 'n'; - case 'ᴏ' -> 'o'; - case 'ᴘ' -> 'p'; - case 'ǫ' -> 'q'; - case 'ʀ' -> 'r'; - case 'ꜱ' -> 's'; - case 'ᴛ' -> 't'; - case 'ᴜ' -> 'u'; - case 'ᴠ' -> 'v'; - case 'ᴡ' -> 'w'; - case 'x' -> 'x'; - case 'ʏ' -> 'y'; - case 'ᴢ' -> 'z'; - default -> ch; - }); - } - return out.toString(); - } - - @Override - protected void defineSettings(List defs) { - defs.add(SettingDef.bind(toggleBind, BindMode.PRESS)); - defs.add(SettingDef.number(scale)); - defs.add(SettingDef.mode(colorMode)); - defs.add(SettingDef.mode(bgEffect)); - defs.add(SettingDef.number(bgAlpha).visibleWhen(this::isThemeMode)); - defs.add(SettingDef.number(blurAlpha).visibleWhen(this::hasEffect)); - defs.add(SettingDef.color(bg).visibleWhen(this::isCustomMode)); - defs.add(SettingDef.color(bg2).visibleWhen(this::isCustomMode)); - defs.add(SettingDef.colorNoAlpha(stroke).visibleWhen(this::isCustomMode)); - defs.add(SettingDef.colorNoAlpha(titleColor).visibleWhen(this::isCustomMode)); - defs.add(SettingDef.colorNoAlpha(labelColor).visibleWhen(this::isCustomMode)); - defs.add(SettingDef.colorNoAlpha(valueColor).visibleWhen(this::isCustomMode)); - defs.add(SettingDef.colorNoAlpha(footerColor).visibleWhen(this::isCustomMode)); - defs.add(SettingDef.mode(textEffect)); - defs.add(SettingDef.number(textEffectSpeed).visibleWhen(() -> textEffect.get() != HudTextEffects.Effect.NONE)); - defs.add(SettingDef.group(textEffectSections).visibleWhen(() -> textEffect.get() != HudTextEffects.Effect.NONE)); - } - - @Override - public void applyDefaultPosition(int screenW, int screenH) { - this.x = 16.0f; - this.y = screenH * 0.5f - 96.0f; - } - - @Override - public boolean usesEngineRenderer() { - return true; - } - - @Override - public HudPhase getHudPhase() { - return HudPhase.BEFORE_CHAT; - } - - @Override - public void onTick() { - Minecraft mc = Minecraft.getInstance(); - if (!toggleInit) { - if (mc == null || mc.getWindow() == null) { - return; - } - initToggleState(); - toggleInit = true; - } - - String combo = toggleBind.get(); - if (combo == null || combo.isBlank() || "NONE".equalsIgnoreCase(combo)) { - return; - } - if (mc == null || mc.getWindow() == null) { - return; - } - - boolean pressed = ClientScreen.current() == null && KeyManager.isComboHeldAllowScreen(combo); - if (!pressed) { - toggleArmed = true; - return; - } - if (toggleArmed) { - toggleNoRenderHidden(); - toggleArmed = false; - } - } - - @Override - public void renderEngine(Renderer2D renderer, - TextRenderer textRenderer, - GuiGraphicsExtractor ctx, - float tickDelta, - int screenW, - int screenH) { - Minecraft mc = Minecraft.getInstance(); - boolean preview = DraggableHudElementRegistry.isForceVisible(); - if (mc == null || (mc.level == null && !preview)) { - width = 0.0f; - height = 0.0f; - return; - } - if (!preview && isHiddenByNoRender()) { - width = 0.0f; - height = 0.0f; - return; - } - if (!preview && !isEnabled()) { - width = 0.0f; - height = 0.0f; - return; - } - - Objective objective = resolveObjective(); - if (objective == null && !preview) { - width = 0.0f; - height = 0.0f; - return; - } - - updatePalette(); - Component title = resolveTitle(objective, preview); - lines.clear(); - lines.addAll(resolveLines(objective, preview)); - if (lines.isEmpty()) { - width = 0.0f; - height = 0.0f; - return; - } - - float baseScale = HudScale.scale(screenW, screenH) * scale.get().floatValue(); - float padX = BASE_PAD_X * baseScale; - float padY = BASE_PAD_Y * baseScale; - float rowGap = BASE_ROW_GAP * baseScale; - float scoreGap = BASE_SCORE_GAP * baseScale; - float footerGap = BASE_FOOTER_GAP * baseScale; - float radius = BASE_RADIUS * baseScale; - float titleScale = BASE_TITLE_SCALE * baseScale; - float textScale = BASE_TEXT_SCALE * baseScale; - float time = (float) (net.minecraft.util.Util.getMillis() / 1000.0); - HudTextEffects.Effect effect = textEffect.get(); - int effectSpeed = textEffectSpeed.get(); - - TextRenderer fallback = textRenderer != null ? textRenderer : TextRenderer.get(); - TextRenderer titleRenderer = Fonts.renderer("Iosevka", FontInfo.Type.BoldItalic, fallback); - TextRenderer bodyRenderer = Fonts.renderer("Iosevka", FontInfo.Type.Regular, titleRenderer); - - Component normalizedTitle = normalizeHeaderTitle(title); - - List> titleLines = splitStyledLines(normalizedTitle, uiTitleColor); - float titleW = measureStyledLineWidth(titleRenderer, titleLines, titleScale); - float titleH = rendererHeight(titleRenderer, titleScale); - - float textH = rendererHeight(bodyRenderer, textScale); - float maxLabelW = 0.0f; - float maxValueW = 0.0f; - float maxFooterW = 0.0f; - for (SidebarLine line : lines) { - if (line.footer()) { - maxFooterW = Math.max(maxFooterW, measureStyledText(bodyRenderer, line.label(), uiFooterColor, textScale)); - continue; - } - maxLabelW = Math.max(maxLabelW, measureStyledText(bodyRenderer, line.label(), uiLabelColor, textScale)); - if (!isBlankText(line.value())) { - maxValueW = Math.max(maxValueW, measureStyledText(bodyRenderer, line.value(), uiValueColor, textScale)); - } - } - - displayLabelWidth = smoothWidth(displayLabelWidth, maxLabelW); - displayValueWidth = smoothWidth(displayValueWidth, maxValueW); - displayFooterWidth = smoothWidth(displayFooterWidth, maxFooterW); - - float widestRow = displayLabelWidth + (displayValueWidth > 0.0f ? scoreGap + displayValueWidth : 0.0f); - float widest = Math.max(titleW, Math.max(widestRow, displayFooterWidth)); - width = padX * 2.0f + widest; - float titleLineGap = BASE_TITLE_LINE_GAP * baseScale; - float titleBlockHeight = titleLines.size() * titleH + Math.max(0, titleLines.size() - 1) * titleLineGap; - float headerHeight = titleBlockHeight + padY * 2.0f; - - int footerCount = 0; - int bodyCount = 0; - for (SidebarLine line : lines) { - if (line.footer()) { - footerCount++; - } else { - bodyCount++; - } - } - - float rowsH = bodyCount > 0 ? bodyCount * textH + Math.max(0, bodyCount - 1) * rowGap : 0.0f; - float footersH = footerCount > 0 ? footerCount * textH + Math.max(0, footerCount - 1) * rowGap : 0.0f; - float footerSectionGap = footerCount > 0 && bodyCount > 0 ? footerGap : 0.0f; - height = headerHeight + padY + rowsH + footerSectionGap + footersH + padY; - - boolean blurEnabled = isBlurEffect(); - if (blurEnabled) { - drawBlur(x, y, width, height, radius, uiBgPrimary); - } - drawBackground(renderer, x, y, width, height, radius, baseScale); - drawHeader(renderer, x, y, width, headerHeight, radius); - - float cursorY = y + headerHeight + padY; - float titleY = y + (headerHeight - titleBlockHeight) * 0.5f; - - renderStyledLinesCentered(renderer, titleRenderer, titleScale, x, width, titleY, titleLines, titleLineGap, - textEffectSections.get("header"), effect, effectSpeed, time); - - boolean footerStarted = false; - for (SidebarLine line : lines) { - if (line.footer()) { - if (!footerStarted && bodyCount > 0) { - cursorY += footerGap; - footerStarted = true; - } - renderStyledText(renderer, bodyRenderer, textScale, x + padX, cursorY, line.label(), uiFooterColor, - textEffectSections.get("footer"), effect, effectSpeed, time, 0.0f); - cursorY += textH + rowGap; - continue; - } - - renderStyledText(renderer, bodyRenderer, textScale, x + padX, cursorY, line.label(), uiLabelColor, - textEffectSections.get("labels"), effect, effectSpeed, time, 0.0f); - if (!isBlankText(line.value())) { - float valueW = measureStyledText(bodyRenderer, line.value(), uiValueColor, textScale); - float valueX = x + width - padX - valueW; - renderStyledText(renderer, bodyRenderer, textScale, valueX, cursorY, line.value(), uiValueColor, - textEffectSections.get("values"), effect, effectSpeed, time, 0.2f); - } - cursorY += textH + rowGap; - } - } - - @Override - public boolean supportsWidgetAnchoring() { - return true; - } - - private Objective resolveObjective() { - Minecraft mc = Minecraft.getInstance(); - if (mc == null || mc.level == null || mc.player == null) { - return null; - } - - net.minecraft.world.scores.Scoreboard scoreboard = mc.level.getScoreboard(); - Objective objective = null; - PlayerTeam team = scoreboard.getPlayersTeam(mc.player.getScoreboardName()); - if (team != null) { - DisplaySlot slot = team.getColor().map(net.minecraft.world.scores.TeamColor::displaySlot).orElse(null); - if (slot != null) { - objective = scoreboard.getDisplayObjective(slot); - } - } - return objective != null ? objective : scoreboard.getDisplayObjective(DisplaySlot.SIDEBAR); - } - - private Component resolveTitle(Objective objective, boolean preview) { - if (objective == null) { - return preview ? Component.literal("Scoreboard") : Component.empty(); - } - Component title = LegacyTextUtil.convertLegacyCodes(objective.getDisplayName()); - return isBlankText(title) ? Component.literal("Scoreboard") : title; - } - - private List resolveLines(Objective objective, boolean preview) { - if (objective == null) { - return preview ? previewLinesSample : List.of(); - } - - net.minecraft.world.scores.Scoreboard scoreboard = objective.getScoreboard(); - NumberFormat numberFormat = objective.numberFormatOrDefault(StyledFormat.SIDEBAR_DEFAULT); - List resolved = new ArrayList<>(); - List visibleEntries = scoreboard.listPlayerScores(objective) - .stream() - .filter(entry -> !entry.isHidden()) - .sorted(ENTRY_COMPARATOR) - .limit(MAX_ENTRIES) - .toList(); - - visibleEntries.forEach(entry -> { - PlayerTeam team = scoreboard.getPlayersTeam(entry.owner()); - Component label = LegacyTextUtil.convertLegacyCodes(PlayerTeam.formatNameForTeam(team, entry.ownerName())); - Component value = LegacyTextUtil.convertLegacyCodes(entry.formatValue(numberFormat)); - if (isBlankText(label) && isBlankText(value)) { - return; - } - resolved.add(new SidebarLine(label, value, isBlankText(value))); - }); - - ServerDumpUtil.dumpScoreboardSidebar( - objective, - visibleEntries, - resolved.stream().map(line -> line.label().getString()).toList(), - resolved.stream().map(line -> line.value().getString()).toList() - ); - - return resolved.isEmpty() && preview ? previewLinesSample : resolved; - } - - private void toggleNoRenderHidden() { - NoRender noRender = Modules.get(NoRender.class); - if (noRender == null) return; - noRender.setHideScoreboardRaw(!noRender.hideScoreboardRaw()); - } - - private void initToggleState() { - Minecraft mc = Minecraft.getInstance(); - toggleArmed = true; - String combo = toggleBind.get(); - if (combo != null && !combo.isBlank() && !"NONE".equalsIgnoreCase(combo) - && mc != null && ClientScreen.current() == null - && KeyManager.isComboHeldAllowScreen(combo)) { - toggleArmed = false; - } - } - - private void drawBlur(float x, float y, float w, float h, float radius, int tintRgb) { - if (!hasEffect()) return; - float quality = hud.getBlurRadius(); - float brightness = 1.0f; - float alpha = blurAlpha.get() / 255f; - Renderer2D.COLOR.blurRect(x, y, w, h, radius, quality, brightness, alpha, 0xFFFFFF); - } - - private void updatePalette() { - if (isThemeMode()) { - int panelAlpha = bgAlpha.get(); - uiBgPrimary = HudRenderUtil.setAlpha(theme().windowBg(), panelAlpha); - uiBgSecondary = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme().surface(), theme().windowHeader(), 0.35f), - panelAlpha - ); - uiHeaderBg = HudRenderUtil.setAlpha(theme().windowHeader(), panelAlpha); - uiStroke = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme().windowStroke(), theme().strokeSoft(), 0.4f), - Math.min(panelAlpha, 190) - ); - uiTitleColor = theme().textPrimary(); - uiLabelColor = theme().textMuted(); - uiValueColor = theme().textPrimary(); - uiFooterColor = HudRenderUtil.mixColor(theme().accent(), theme().textPrimary(), 0.35f); - return; - } - - uiBgPrimary = bg.getArgb(); - uiBgSecondary = bg2.getArgb(); - uiHeaderBg = HudRenderUtil.mixColor(bg2.getArgb(), bg.getArgb(), 0.2f); - uiStroke = stroke.getArgb(); - uiTitleColor = titleColor.getArgb(); - uiLabelColor = labelColor.getArgb(); - uiValueColor = valueColor.getArgb(); - uiFooterColor = footerColor.getArgb(); - } - - private void drawBackground(Renderer2D renderer, - float x, - float y, - float width, - float height, - float radius, - float drawScale) { - if (isThemeMode()) { - HudRenderUtil.drawHudBackground(renderer, x, y, width, height, radius, BASE_SOFTNESS, uiBgPrimary, true); - } else { - renderer.roundedRectGradientQuad(x, y, width, height, radius, BASE_SOFTNESS, - uiBgPrimary, uiBgSecondary, uiBgPrimary, uiBgSecondary); - } - - renderer.roundedRectStroke( - x, y, width, height, radius, BASE_SOFTNESS, - Math.max(0.5f, BASE_STROKE * drawScale), uiStroke - ); - } - - private void drawHeader(Renderer2D renderer, - float x, - float y, - float width, - float headerHeight, - float radius) { - renderer.roundedRectCorners(x, y, width, headerHeight, radius, radius, 0.0f, 0.0f, BASE_SOFTNESS, uiHeaderBg); - } - - private boolean isThemeMode() { - return COLOR_THEME.equals(colorMode.get()); - } - - private boolean isCustomMode() { - return COLOR_CUSTOM.equals(colorMode.get()); - } - - private boolean isBlurEffect() { - return EFFECT_BLUR.equals(bgEffect.get()); - } - - private boolean hasEffect() { - return !EFFECT_NONE.equals(bgEffect.get()); - } - - private record SidebarLine(Component label, Component value, boolean footer) { - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Fps.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Fps.java deleted file mode 100644 index 50a5475..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Fps.java +++ /dev/null @@ -1,309 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.features.gui.hud.draggable.impl.stats; - -import dev.loki.lovisual.config.values.color.RGBAColorValue; -import dev.loki.lovisual.config.values.color.RGBColorValue; -import dev.loki.lovisual.config.values.mode.EnumValue; -import dev.loki.lovisual.config.values.mode.ModeValue; -import dev.loki.lovisual.config.values.primitive.BooleanValue; -import dev.loki.lovisual.config.values.primitive.NumberValue; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.resources.Identifier; -import net.minecraft.util.Util; -import dev.loki.lovisual.features.gui.hud.registry.HudElementInfo; -import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; -import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; -import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; -import dev.loki.lovisual.features.gui.hud.script.CompactHudStatModel; -import dev.loki.lovisual.features.gui.hud.script.ScriptableHudStatWidget; -import dev.loki.lovisual.features.gui.hud.script.ScriptedCompactHudStatRenderer; -import dev.loki.lovisual.render.engine.animation.AnimationUtility; -import dev.loki.lovisual.render.engine.math.HudScale; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.text.font.info.FontInfo; -import dev.loki.lovisual.render.engine.text.font.Fonts; -import dev.loki.lovisual.render.engine.text.render.TextRenderer; -import dev.loki.lovisual.util.FastFps; - -import static dev.loki.lovisual.features.theme.Theme.theme; - -@HudElementInfo( - id = "fps", - displayName = "FPS", - enabledByDefault = true, - order = 120 -) -public final class Fps extends DraggableHudElement implements ScriptableHudStatWidget { - - private static final Identifier FPS_ICON = Identifier.fromNamespaceAndPath("lovisual", "textures/hud/elements/fps.png"); - private static final float SCALE_MULT = 0.68f; - private static final float BOX_HEIGHT = 20f; - private static final float BOX_RADIUS = 5f; - private static final float BOX_SOFTNESS = 1.0f; - private static final float STROKE_WIDTH = 0.55f; - private static final float ICON_SIZE = 12.5f; - private static final float ICON_X = 5f; - private static final float DIVIDER_X = 21f; - private static final float DIVIDER_Y = 4f; - private static final float DIVIDER_WIDTH = 0.6f; - private static final float TEXT_LEFT_X = 26f; - private static final float VALUE_UNIT_GAP = 2f; - private static final float RIGHT_PAD = 5f; - private static final String COLOR_THEME = "Theme"; - private static final String COLOR_CUSTOM = "Custom"; - private static final String EFFECT_NONE = "None"; - private static final String EFFECT_BLUR = "Blur"; - private static final String EFFECT_GLASS = "Glass"; - private static final long DIGIT_ANIMATION_DURATION_MS = 200L; - private static final float DIGIT_ANIMATION_OFFSET = 8.0f; - - private final HudGlobalConfig hud = HudGlobalConfig.get(); - private final NumberValue scale = num("fps_scale", 2.37, 0.5, 5.0); - private final ModeValue colorMode = mode("fps_color_mode", "color_mode", "Theme", new String[]{COLOR_THEME, COLOR_CUSTOM}); - private final RGBColorValue iconColor = visibleWhen(colorNoAlpha("fps_icon_color", "#FFFFFF"), this::isCustomMode); - private final RGBColorValue valueColor = visibleWhen(colorNoAlpha("fps_value_color", "#FFFFFF"), this::isCustomMode); - private final RGBColorValue metaColor = visibleWhen(colorNoAlpha("fps_meta_color", "#9B9B9B"), this::isCustomMode); - private final ModeValue bgEffect = mode("fps_bg_effect", "bg_effect", "Blur", new String[]{EFFECT_NONE, EFFECT_BLUR, EFFECT_GLASS}); - private final RGBAColorValue bg = visibleWhen(color("fps_bg", "#F7343434"), () -> isCustomMode() && !isGlassEffect()); - private final RGBAColorValue bg2 = visibleWhen(color("fps_bg_secondary", "#F7161616"), () -> isCustomMode() && !isGlassEffect()); - private final RGBColorValue stroke = visibleWhen(colorNoAlpha("fps_stroke", "#5A5A5A"), () -> isCustomMode() && !isGlassEffect()); - private final NumberValue bgAlpha = visibleWhen(num("fps_bg_alpha", 133, 0, 255), () -> isThemeMode() && !isGlassEffect()); - private final NumberValue blurAlpha = visibleWhen(num("fps_blur_alpha", 255, 0, 255), this::hasEffect); - private final EnumValue labelEffect = - enumSetting("fps_label_effect", HudTextEffects.Effect.FLOW, - HudTextEffects.Effect.NONE, HudTextEffects.Effect.MIX, HudTextEffects.Effect.FLOW, - HudTextEffects.Effect.PULSE, HudTextEffects.Effect.STRIPE); - private final NumberValue labelEffectSpeed = - visibleWhen(num("fps_label_effect_speed", 18, 1, 60), - () -> labelEffect.get() != HudTextEffects.Effect.NONE); - private final BooleanValue digitAnimation = bool("fps_digit_animation", false); - private final NumberValue digitPollMs = - visibleWhen(num("fps_digit_poll_ms", 495, 50, 1000), digitAnimation::get); - private final CompactHudStatModel scriptModel = new CompactHudStatModel("fps"); - - private String shownFps = "0"; - private String previousShownFps = ""; - private long digitAnimStartMs = 0L; - private long lastDigitPollMs = 0L; - private float displayValueWidth = -1f; - - private int uiBgPrimary; - private int uiBgSecondary; - private int uiStroke; - private int uiIconColor; - private int uiValueColor; - private int uiMetaColor; - - private static String widerText(String a, String b) { - return a != null && b != null && a.length() < b.length() ? b : a; - } - - private static float smoothWidth(float current, float target) { - if (current < 0.0f) return target; - if (target >= current) return target; - float next = AnimationUtility.approach(current, target, AnimationUtility.deltaTime(), 12.0f); - return AnimationUtility.snap(next, target, 0.25f); - } - - @Override - public void applyDefaultPosition(int screenW, int screenH) { - this.x = 16f; - this.y = 20f; - } - - @Override - public boolean usesEngineRenderer() { - return true; - } - - @Override - public void renderEngine(Renderer2D renderer, - TextRenderer textRenderer, - GuiGraphicsExtractor ctx, - float tickDelta, - int screenW, - int screenH) { - boolean preview = DraggableHudElementRegistry.isForceVisible(); - if (Minecraft.getInstance() == null && !preview) { - width = 0f; - height = 0f; - scriptModel.setVisible(false); - return; - } - if (!preview && !isEnabled()) { - width = 0f; - height = 0f; - scriptModel.setVisible(false); - return; - } - - int fps = Math.max(0, FastFps.getFps()); - long nowMs = Util.getMillis(); - String value = resolveDisplayedFps(Integer.toString(fps), nowMs); - String unit = "FPS"; - float digitAnimProgress = 1.0f; - if (digitAnimation.get() && digitAnimStartMs > 0L) { - digitAnimProgress = AnimationUtility.clamp01( - (nowMs - digitAnimStartMs) / (float) DIGIT_ANIMATION_DURATION_MS - ); - } - String widthValue = digitAnimation.get() ? widerText(value, previousShownFps) : value; - updatePalette(); - - TextRenderer fallback = textRenderer != null ? textRenderer : TextRenderer.get(); - TextRenderer valueRenderer = Fonts.renderer("Onest", FontInfo.Type.Regular, fallback); - TextRenderer metaRenderer = Fonts.renderer("OnestMedium", FontInfo.Type.Regular, valueRenderer); - - float drawScale = HudScale.scale(screenW, screenH) - * (hud.getFontSize() / 18f) - * SCALE_MULT - * scale.get().floatValue(); - - valueRenderer.begin(drawScale, true, false); - float valueW = (float) valueRenderer.getWidth(widthValue, false); - float valueH = (float) valueRenderer.getHeight(false); - valueRenderer.end(); - - metaRenderer.begin(drawScale, true, false); - float unitW = (float) metaRenderer.getWidth(unit, false); - float unitH = (float) metaRenderer.getHeight(false); - metaRenderer.end(); - - float boxH = BOX_HEIGHT * drawScale; - float iconSize = ICON_SIZE * drawScale; - float valueX = x + TEXT_LEFT_X * drawScale; - float unitGap = VALUE_UNIT_GAP * drawScale; - displayValueWidth = smoothWidth(displayValueWidth, valueW); - float boxW = (valueX - x) + displayValueWidth + unitGap + unitW + RIGHT_PAD * drawScale; - width = boxW; - height = boxH; - - float baseX = x; - float baseY = y; - float iconX = baseX + ICON_X * drawScale; - float iconY = baseY + (boxH - iconSize) * 0.5f; - float dividerX = baseX + DIVIDER_X * drawScale; - float dividerY = baseY + DIVIDER_Y * drawScale; - float dividerW = Math.max(0.5f, DIVIDER_WIDTH * drawScale); - float dividerH = Math.max(0.5f, boxH - 2f * DIVIDER_Y * drawScale); - float textY = baseY + (boxH - Math.max(valueH, unitH)) * 0.5f; - float unitX = valueX + displayValueWidth + unitGap; - - int resolvedIconColor = uiIconColor; - boolean useLabelEffect = labelEffect.get() != HudTextEffects.Effect.NONE; - float time = useLabelEffect ? (float) (Util.getMillis() / 1000.0) : 0.0f; - if (useLabelEffect) { - resolvedIconColor = HudTextEffects.animatedColor(resolvedIconColor, labelEffect.get(), - labelEffectSpeed.get(), time, 0.0f); - } - - float radius = BOX_RADIUS * drawScale; - scriptModel.setVisible(true); - scriptModel.setRoot(baseX, baseY, boxW, boxH, radius, drawScale); - scriptModel.background().set(bgEffect.get(), isThemeMode(), blurAlpha.get() / 255.0f, - uiBgPrimary, uiBgSecondary, uiStroke, Math.max(0.5f, STROKE_WIDTH * drawScale), BOX_SOFTNESS); - scriptModel.icon().texture(FPS_ICON.toString(), iconX - baseX, iconY - baseY, iconSize, iconSize, resolvedIconColor); - scriptModel.divider().set(dividerX - baseX, dividerY - baseY, dividerW, dividerH, - HudRenderUtil.setAlpha(uiMetaColor, 0x52)); - scriptModel.value().set(value, "Onest", drawScale, valueX - baseX, textY - baseY, displayValueWidth, uiValueColor); - scriptModel.unit().set(unit, "OnestMedium", drawScale, unitX - baseX, textY - baseY, unitW, uiMetaColor); - scriptModel.extra().hidden(); - scriptModel.animation().setLabelEffect(labelEffect.get().name(), labelEffectSpeed.get(), time) - .setDigitAnimation(digitAnimation.get(), previousShownFps, digitAnimProgress, DIGIT_ANIMATION_OFFSET); - scriptModel.data("fps", fps) - .data("valueText", value) - .data("unitText", unit); - ScriptedCompactHudStatRenderer.INSTANCE.render(scriptModel, renderer, fallback, ctx, tickDelta); - } - - @Override - public boolean supportsWidgetAnchoring() { - return true; - } - - @Override - public CompactHudStatModel compactStatModel() { - return scriptModel; - } - - private void updatePalette() { - if (isThemeMode()) { - int panelAlpha = bgAlpha.get(); - uiBgPrimary = HudRenderUtil.setAlpha(theme().windowBg(), panelAlpha); - uiBgSecondary = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme().surface(), theme().windowHeader(), 0.35f), - panelAlpha - ); - uiStroke = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme().windowStroke(), theme().strokeSoft(), 0.4f), - Math.min(panelAlpha, 190) - ); - uiIconColor = theme().textPrimary(); - uiValueColor = theme().textPrimary(); - uiMetaColor = theme().textMuted(); - return; - } - - uiBgPrimary = bg.getArgb(); - uiBgSecondary = bg2.getArgb(); - uiStroke = stroke.getArgb(); - uiIconColor = iconColor.getArgb(); - uiValueColor = valueColor.getArgb(); - uiMetaColor = metaColor.getArgb(); - } - - private String resolveDisplayedFps(String rawValue, long nowMs) { - if (!digitAnimation.get()) { - shownFps = rawValue; - previousShownFps = rawValue; - digitAnimStartMs = 0L; - lastDigitPollMs = nowMs; - return rawValue; - } - - if (shownFps == null || shownFps.isEmpty()) { - shownFps = rawValue; - previousShownFps = rawValue; - digitAnimStartMs = 0L; - lastDigitPollMs = nowMs; - return shownFps; - } - - if (nowMs - lastDigitPollMs < digitPollMs.get()) { - return shownFps; - } - - lastDigitPollMs = nowMs; - if (!rawValue.equals(shownFps)) { - previousShownFps = shownFps; - shownFps = rawValue; - digitAnimStartMs = nowMs; - } - return shownFps; - } - - private boolean isThemeMode() { - return COLOR_THEME.equals(colorMode.get()); - } - - private boolean isCustomMode() { - return COLOR_CUSTOM.equals(colorMode.get()); - } - - private boolean isGlassEffect() { - return EFFECT_GLASS.equals(bgEffect.get()); - } - - private boolean hasEffect() { - return !EFFECT_NONE.equals(bgEffect.get()); - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Memory.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Memory.java deleted file mode 100644 index 3709010..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Memory.java +++ /dev/null @@ -1,304 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.features.gui.hud.draggable.impl.stats; - -import dev.loki.lovisual.config.values.color.RGBAColorValue; -import dev.loki.lovisual.config.values.color.RGBColorValue; -import dev.loki.lovisual.config.values.mode.EnumValue; -import dev.loki.lovisual.config.values.mode.ModeValue; -import dev.loki.lovisual.config.values.primitive.NumberValue; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.resources.Identifier; -import net.minecraft.util.Util; -import dev.loki.lovisual.features.gui.hud.registry.HudElementInfo; -import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; -import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; -import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; -import dev.loki.lovisual.features.gui.hud.script.CompactHudStatModel; -import dev.loki.lovisual.features.gui.hud.script.ScriptableHudStatWidget; -import dev.loki.lovisual.features.gui.hud.script.ScriptedCompactHudStatRenderer; -import dev.loki.lovisual.render.engine.animation.AnimationUtility; -import dev.loki.lovisual.render.engine.math.HudScale; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.text.font.info.FontInfo; -import dev.loki.lovisual.render.engine.text.font.Fonts; -import dev.loki.lovisual.render.engine.text.render.TextRenderer; - -import static dev.loki.lovisual.features.theme.Theme.theme; - -@HudElementInfo( - id = "memory", - displayName = "Memory", - enabledByDefault = true, - order = 130 -) -public final class Memory extends DraggableHudElement implements ScriptableHudStatWidget { - - private static final Identifier MEMORY_ICON = Identifier.fromNamespaceAndPath("lovisual", "textures/hud/elements/memory.png"); - private static final float SCALE_MULT = 0.68f; - private static final float BOX_HEIGHT = 20f; - private static final float BOX_RADIUS = 5f; - private static final float BOX_SOFTNESS = 1.0f; - private static final float STROKE_WIDTH = 0.55f; - private static final float ICON_SIZE = 11f; - private static final float ICON_X = 5f; - private static final float DIVIDER_X = 21f; - private static final float DIVIDER_Y = 4f; - private static final float DIVIDER_WIDTH = 0.6f; - private static final float TEXT_LEFT_X = 26f; - private static final float VALUE_UNIT_GAP = 2f; - private static final float EXTRA_GAP = 5f; - private static final float RIGHT_PAD = 5f; - private static final String COLOR_THEME = "Theme"; - private static final String COLOR_CUSTOM = "Custom"; - private static final String EFFECT_NONE = "None"; - private static final String EFFECT_BLUR = "Blur"; - private static final String EFFECT_GLASS = "Glass"; - - private final HudGlobalConfig hud = HudGlobalConfig.get(); - private final NumberValue scale = num("memory_scale", 2.37, 0.5, 5.0); - private final ModeValue colorMode = mode("memory_color_mode", "color_mode", "Theme", new String[]{COLOR_THEME, COLOR_CUSTOM}); - private final RGBColorValue iconColor = visibleWhen(colorNoAlpha("memory_icon_color", "#FFFFFF"), this::isCustomMode); - private final RGBColorValue valueColor = visibleWhen(colorNoAlpha("memory_value_color", "#FFFFFF"), this::isCustomMode); - private final RGBColorValue metaColor = visibleWhen(colorNoAlpha("memory_meta_color", "#9B9B9B"), this::isCustomMode); - private final RGBColorValue extraColor = visibleWhen(colorNoAlpha("memory_extra_color", "#BEBEBE"), this::isCustomMode); - private final ModeValue bgEffect = mode("memory_bg_effect", "bg_effect", "Blur", new String[]{EFFECT_NONE, EFFECT_BLUR, EFFECT_GLASS}); - private final RGBAColorValue bg = visibleWhen(color("memory_bg", "#F7343434"), () -> isCustomMode() && !isGlassEffect()); - private final RGBAColorValue bg2 = visibleWhen(color("memory_bg_secondary", "#F7161616"), () -> isCustomMode() && !isGlassEffect()); - private final RGBColorValue stroke = visibleWhen(colorNoAlpha("memory_stroke", "#5A5A5A"), () -> isCustomMode() && !isGlassEffect()); - private final NumberValue bgAlpha = visibleWhen(num("memory_bg_alpha", 135, 0, 255), () -> isThemeMode() && !isGlassEffect()); - private final NumberValue blurAlpha = visibleWhen(num("memory_blur_alpha", 255, 0, 255), this::hasEffect); - private final EnumValue labelEffect = - enumSetting("memory_label_effect", HudTextEffects.Effect.NONE, - HudTextEffects.Effect.NONE, HudTextEffects.Effect.MIX, HudTextEffects.Effect.FLOW, - HudTextEffects.Effect.PULSE, HudTextEffects.Effect.STRIPE); - private final NumberValue labelEffectSpeed = - visibleWhen(num("memory_label_effect_speed", 18, 1, 60), - () -> labelEffect.get() != HudTextEffects.Effect.NONE); - private final CompactHudStatModel scriptModel = new CompactHudStatModel("memory"); - private final StringBuilder valueBuilder = new StringBuilder(24); - private long lastUsedMiB = Long.MIN_VALUE; - private long lastMaxMiB = Long.MIN_VALUE; - private int lastPercent = Integer.MIN_VALUE; - private String cachedValue = "0/0"; - private String cachedExtra = "[0%]"; - private float displayValueWidth = -1f; - private float displayExtraWidth = -1f; - private int uiBgPrimary; - private int uiBgSecondary; - private int uiStroke; - private int uiIconColor; - private int uiValueColor; - private int uiMetaColor; - private int uiExtraColor; - - private static long toMiB(long bytes) { - return bytes / 1024L / 1024L; - } - - private static float smoothWidth(float current, float target) { - if (current < 0.0f) return target; - if (target >= current) return target; - float next = AnimationUtility.approach(current, target, AnimationUtility.deltaTime(), 12.0f); - return AnimationUtility.snap(next, target, 0.25f); - } - - @Override - public void applyDefaultPosition(int screenW, int screenH) { - this.x = 16f; - this.y = 44f; - } - - @Override - public boolean usesEngineRenderer() { - return true; - } - - @Override - public void renderEngine(Renderer2D renderer, - TextRenderer textRenderer, - GuiGraphicsExtractor ctx, - float tickDelta, - int screenW, - int screenH) { - boolean preview = DraggableHudElementRegistry.isForceVisible(); - if (Minecraft.getInstance() == null && !preview) { - width = 0f; - height = 0f; - scriptModel.setVisible(false); - return; - } - if (!preview && !isEnabled()) { - width = 0f; - height = 0f; - scriptModel.setVisible(false); - return; - } - - Runtime rt = Runtime.getRuntime(); - long max = rt.maxMemory(); - long total = rt.totalMemory(); - long free = rt.freeMemory(); - long used = Math.max(0L, total - free); - long usedMiB = toMiB(used); - long maxMiB = toMiB(max > 0L ? max : total); - int percent = max > 0L ? (int) Math.round((double) used * 100.0 / (double) max) : 0; - percent = Math.max(0, Math.min(100, percent)); - updateMemoryStrings(usedMiB, maxMiB, percent); - updatePalette(); - - TextRenderer fallback = textRenderer != null ? textRenderer : TextRenderer.get(); - TextRenderer valueRenderer = Fonts.renderer("Onest", FontInfo.Type.Regular, fallback); - TextRenderer metaRenderer = Fonts.renderer("OnestMedium", FontInfo.Type.Regular, valueRenderer); - - float drawScale = HudScale.scale(screenW, screenH) - * (hud.getFontSize() / 18f) - * SCALE_MULT - * scale.get().floatValue(); - - valueRenderer.begin(drawScale, true, false); - float valueW = (float) valueRenderer.getWidth(cachedValue, false); - float valueH = (float) valueRenderer.getHeight(false); - valueRenderer.end(); - - metaRenderer.begin(drawScale, true, false); - float unitW = (float) metaRenderer.getWidth("MB", false); - float unitH = (float) metaRenderer.getHeight(false); - float extraW = (float) metaRenderer.getWidth(cachedExtra, false); - metaRenderer.end(); - - float boxH = BOX_HEIGHT * drawScale; - float iconSize = ICON_SIZE * drawScale; - float valueX = x + TEXT_LEFT_X * drawScale; - float unitGap = VALUE_UNIT_GAP * drawScale; - float extraGap = EXTRA_GAP * drawScale; - displayValueWidth = smoothWidth(displayValueWidth, valueW); - displayExtraWidth = smoothWidth(displayExtraWidth, extraW); - float boxW = (valueX - x) - + displayValueWidth - + unitGap - + unitW - + extraGap - + displayExtraWidth - + RIGHT_PAD * drawScale; - width = boxW; - height = boxH; - - float baseX = x; - float baseY = y; - float iconX = baseX + ICON_X * drawScale; - float iconY = baseY + (boxH - iconSize) * 0.5f; - float dividerX = baseX + DIVIDER_X * drawScale; - float dividerY = baseY + DIVIDER_Y * drawScale; - float dividerW = Math.max(0.5f, DIVIDER_WIDTH * drawScale); - float dividerH = Math.max(0.5f, boxH - 2f * DIVIDER_Y * drawScale); - float textY = baseY + (boxH - Math.max(valueH, unitH)) * 0.5f; - float unitX = valueX + displayValueWidth + unitGap; - float extraX = unitX + unitW + extraGap; - - int resolvedIconColor = uiIconColor; - boolean useLabelEffect = labelEffect.get() != HudTextEffects.Effect.NONE; - float time = useLabelEffect ? (float) (Util.getMillis() / 1000.0) : 0.0f; - if (useLabelEffect) { - resolvedIconColor = HudTextEffects.animatedColor(resolvedIconColor, labelEffect.get(), - labelEffectSpeed.get(), time, 0.0f); - } - - float radius = BOX_RADIUS * drawScale; - scriptModel.setVisible(true); - scriptModel.setRoot(baseX, baseY, boxW, boxH, radius, drawScale); - scriptModel.background().set(bgEffect.get(), isThemeMode(), blurAlpha.get() / 255.0f, - uiBgPrimary, uiBgSecondary, uiStroke, Math.max(0.5f, STROKE_WIDTH * drawScale), BOX_SOFTNESS); - scriptModel.icon().texture(MEMORY_ICON.toString(), iconX - baseX, iconY - baseY, iconSize, iconSize, resolvedIconColor); - scriptModel.divider().set(dividerX - baseX, dividerY - baseY, dividerW, dividerH, - HudRenderUtil.setAlpha(uiMetaColor, 0x52)); - scriptModel.value().set(cachedValue, "Onest", drawScale, valueX - baseX, textY - baseY, displayValueWidth, uiValueColor); - scriptModel.unit().set("MB", "OnestMedium", drawScale, unitX - baseX, textY - baseY, unitW, uiMetaColor); - scriptModel.extra().set(cachedExtra, "OnestMedium", drawScale, extraX - baseX, textY - baseY, displayExtraWidth, uiExtraColor); - scriptModel.animation().setLabelEffect(labelEffect.get().name(), labelEffectSpeed.get(), time) - .setDigitAnimation(false, "", 1.0f, 0.0f); - scriptModel.data("usedMiB", usedMiB) - .data("maxMiB", maxMiB) - .data("percent", percent) - .data("valueText", cachedValue) - .data("unitText", "MB") - .data("extraText", cachedExtra); - ScriptedCompactHudStatRenderer.INSTANCE.render(scriptModel, renderer, fallback, ctx, tickDelta); - } - - @Override - public boolean supportsWidgetAnchoring() { - return true; - } - - @Override - public CompactHudStatModel compactStatModel() { - return scriptModel; - } - - private void updateMemoryStrings(long usedMiB, long maxMiB, int percent) { - if (usedMiB == lastUsedMiB && maxMiB == lastMaxMiB && percent == lastPercent) { - return; - } - lastUsedMiB = usedMiB; - lastMaxMiB = maxMiB; - lastPercent = percent; - - valueBuilder.setLength(0); - valueBuilder.append(usedMiB).append('/').append(maxMiB); - cachedValue = valueBuilder.toString(); - cachedExtra = "[" + percent + "%]"; - } - - private void updatePalette() { - if (isThemeMode()) { - int panelAlpha = bgAlpha.get(); - uiBgPrimary = HudRenderUtil.setAlpha(theme().windowBg(), panelAlpha); - uiBgSecondary = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme().surface(), theme().windowHeader(), 0.35f), - panelAlpha - ); - uiStroke = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme().windowStroke(), theme().strokeSoft(), 0.4f), - Math.min(panelAlpha, 190) - ); - uiIconColor = theme().textPrimary(); - uiValueColor = theme().textPrimary(); - uiMetaColor = theme().textMuted(); - uiExtraColor = HudRenderUtil.mixColor(theme().textMuted(), theme().textPrimary(), 0.18f); - return; - } - - uiBgPrimary = bg.getArgb(); - uiBgSecondary = bg2.getArgb(); - uiStroke = stroke.getArgb(); - uiIconColor = iconColor.getArgb(); - uiValueColor = valueColor.getArgb(); - uiMetaColor = metaColor.getArgb(); - uiExtraColor = extraColor.getArgb(); - } - - private boolean isThemeMode() { - return COLOR_THEME.equals(colorMode.get()); - } - - private boolean isCustomMode() { - return COLOR_CUSTOM.equals(colorMode.get()); - } - - private boolean isGlassEffect() { - return EFFECT_GLASS.equals(bgEffect.get()); - } - - private boolean hasEffect() { - return !EFFECT_NONE.equals(bgEffect.get()); - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Ping.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Ping.java deleted file mode 100644 index f42c79b..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Ping.java +++ /dev/null @@ -1,266 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.features.gui.hud.draggable.impl.stats; - -import dev.loki.lovisual.config.values.color.RGBAColorValue; -import dev.loki.lovisual.config.values.color.RGBColorValue; -import dev.loki.lovisual.config.values.mode.EnumValue; -import dev.loki.lovisual.config.values.mode.ModeValue; -import dev.loki.lovisual.config.values.primitive.NumberValue; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.resources.Identifier; -import net.minecraft.util.Util; -import dev.loki.lovisual.features.gui.hud.registry.HudElementInfo; -import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; -import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; -import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; -import dev.loki.lovisual.features.gui.hud.script.CompactHudStatModel; -import dev.loki.lovisual.features.gui.hud.script.ScriptableHudStatWidget; -import dev.loki.lovisual.features.gui.hud.script.ScriptedCompactHudStatRenderer; -import dev.loki.lovisual.render.engine.animation.AnimationUtility; -import dev.loki.lovisual.render.engine.math.HudScale; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.text.font.info.FontInfo; -import dev.loki.lovisual.render.engine.text.font.Fonts; -import dev.loki.lovisual.render.engine.text.render.TextRenderer; -import dev.loki.lovisual.util.player.interaction.NetworkStatsUtil; - -import static dev.loki.lovisual.features.theme.Theme.theme; - -//todo Description -@HudElementInfo( - id = "ping", - displayName = "Ping", - enabledByDefault = false, - order = 150 -) -public final class Ping extends DraggableHudElement implements ScriptableHudStatWidget { - - private static final Identifier PING_ICON = Identifier.fromNamespaceAndPath("lovisual", "textures/hud/elements/ping.png"); - private static final float SCALE_MULT = 0.68f; - private static final float BOX_HEIGHT = 20f; - private static final float BOX_RADIUS = 5f; - private static final float BOX_SOFTNESS = 1.0f; - private static final float STROKE_WIDTH = 0.55f; - private static final float ICON_SIZE = 11f; - private static final float ICON_X = 5f; - private static final float DIVIDER_X = 21f; - private static final float DIVIDER_Y = 4f; - private static final float DIVIDER_WIDTH = 0.6f; - private static final float TEXT_LEFT_X = 26f; - private static final float VALUE_UNIT_GAP = 2f; - private static final float RIGHT_PAD = 5f; - private static final String COLOR_THEME = "Theme"; - private static final String COLOR_CUSTOM = "Custom"; - private static final String EFFECT_NONE = "None"; - private static final String EFFECT_BLUR = "Blur"; - private static final String EFFECT_GLASS = "Glass"; - - private final HudGlobalConfig hud = HudGlobalConfig.get(); - private final NumberValue scale = num("ping_scale", 2.58, 0.5, 5.0); - private final ModeValue colorMode = mode("ping_color_mode", "color_mode", "Theme", new String[]{COLOR_THEME, COLOR_CUSTOM}); - private final RGBColorValue iconColor = visibleWhen(colorNoAlpha("ping_icon_color", "#FFFFFF"), this::isCustomMode); - private final RGBColorValue valueColor = visibleWhen(colorNoAlpha("ping_value_color", "#FFFFFF"), this::isCustomMode); - private final RGBColorValue metaColor = visibleWhen(colorNoAlpha("ping_meta_color", "#9B9B9B"), this::isCustomMode); - private final ModeValue bgEffect = mode("ping_bg_effect", "bg_effect", "None", new String[]{EFFECT_NONE, EFFECT_BLUR, EFFECT_GLASS}); - private final RGBAColorValue bg = visibleWhen(color("ping_bg", "#F7343434"), () -> isCustomMode() && !isGlassEffect()); - private final RGBAColorValue bg2 = visibleWhen(color("ping_bg_secondary", "#F7161616"), () -> isCustomMode() && !isGlassEffect()); - private final RGBColorValue stroke = visibleWhen(colorNoAlpha("ping_stroke", "#5A5A5A"), () -> isCustomMode() && !isGlassEffect()); - private final NumberValue bgAlpha = visibleWhen(num("ping_bg_alpha", 225, 0, 255), () -> isThemeMode() && !isGlassEffect()); - private final NumberValue blurAlpha = visibleWhen(num("ping_blur_alpha", 140, 0, 255), this::hasEffect); - private final EnumValue labelEffect = - enumSetting("ping_label_effect", HudTextEffects.Effect.NONE, - HudTextEffects.Effect.NONE, HudTextEffects.Effect.MIX, HudTextEffects.Effect.FLOW, - HudTextEffects.Effect.PULSE, HudTextEffects.Effect.STRIPE); - private final NumberValue labelEffectSpeed = - visibleWhen(num("ping_label_effect_speed", 18, 1, 60), - () -> labelEffect.get() != HudTextEffects.Effect.NONE); - private final CompactHudStatModel scriptModel = new CompactHudStatModel("ping"); - - private int lastPing = Integer.MIN_VALUE; - private String cachedValue = "--"; - private float displayValueWidth = -1f; - private int uiBgPrimary; - private int uiBgSecondary; - private int uiStroke; - private int uiIconColor; - private int uiValueColor; - private int uiMetaColor; - - private static float smoothWidth(float current, float target) { - if (current < 0.0f) return target; - if (target >= current) return target; - float next = AnimationUtility.approach(current, target, AnimationUtility.deltaTime(), 12.0f); - return AnimationUtility.snap(next, target, 0.25f); - } - - @Override - public void applyDefaultPosition(int screenW, int screenH) { - this.x = 16f; - this.y = 76f; - } - - @Override - public boolean usesEngineRenderer() { - return true; - } - - @Override - public void renderEngine(Renderer2D renderer, - TextRenderer textRenderer, - GuiGraphicsExtractor ctx, - float tickDelta, - int screenW, - int screenH) { - boolean preview = DraggableHudElementRegistry.isForceVisible(); - if (Minecraft.getInstance() == null && !preview) { - width = 0f; - height = 0f; - scriptModel.setVisible(false); - return; - } - if (!preview && !isEnabled()) { - width = 0f; - height = 0f; - scriptModel.setVisible(false); - return; - } - - int ping = NetworkStatsUtil.getPing(Minecraft.getInstance()); - String value = getPingValue(ping); - String unit = "ms"; - updatePalette(); - - TextRenderer fallback = textRenderer != null ? textRenderer : TextRenderer.get(); - TextRenderer valueRenderer = Fonts.renderer("Onest", FontInfo.Type.Regular, fallback); - TextRenderer metaRenderer = Fonts.renderer("OnestMedium", FontInfo.Type.Regular, valueRenderer); - - float drawScale = HudScale.scale(screenW, screenH) - * (hud.getFontSize() / 18f) - * SCALE_MULT - * scale.get().floatValue(); - - valueRenderer.begin(drawScale, true, false); - float valueW = (float) valueRenderer.getWidth(value, false); - float valueH = (float) valueRenderer.getHeight(false); - valueRenderer.end(); - - metaRenderer.begin(drawScale, true, false); - float unitW = (float) metaRenderer.getWidth(unit, false); - float unitH = (float) metaRenderer.getHeight(false); - metaRenderer.end(); - - float boxH = BOX_HEIGHT * drawScale; - float iconSize = ICON_SIZE * drawScale; - float valueX = x + TEXT_LEFT_X * drawScale; - float unitGap = VALUE_UNIT_GAP * drawScale; - displayValueWidth = smoothWidth(displayValueWidth, valueW); - float boxW = (valueX - x) + displayValueWidth + unitGap + unitW + RIGHT_PAD * drawScale; - width = boxW; - height = boxH; - - float baseX = x; - float baseY = y; - float iconX = baseX + ICON_X * drawScale; - float iconY = baseY + (boxH - iconSize) * 0.5f; - float dividerX = baseX + DIVIDER_X * drawScale; - float dividerY = baseY + DIVIDER_Y * drawScale; - float dividerW = Math.max(0.5f, DIVIDER_WIDTH * drawScale); - float dividerH = Math.max(0.5f, boxH - 2f * DIVIDER_Y * drawScale); - float textY = baseY + (boxH - Math.max(valueH, unitH)) * 0.5f; - float unitX = valueX + displayValueWidth + unitGap; - - int resolvedIconColor = uiIconColor; - boolean useLabelEffect = labelEffect.get() != HudTextEffects.Effect.NONE; - float time = useLabelEffect ? (float) (Util.getMillis() / 1000.0) : 0.0f; - if (useLabelEffect) { - resolvedIconColor = HudTextEffects.animatedColor(resolvedIconColor, labelEffect.get(), - labelEffectSpeed.get(), time, 0.0f); - } - - float radius = BOX_RADIUS * drawScale; - scriptModel.setVisible(true); - scriptModel.setRoot(baseX, baseY, boxW, boxH, radius, drawScale); - scriptModel.background().set(bgEffect.get(), isThemeMode(), blurAlpha.get() / 255.0f, - uiBgPrimary, uiBgSecondary, uiStroke, Math.max(0.5f, STROKE_WIDTH * drawScale), BOX_SOFTNESS); - scriptModel.icon().texture(PING_ICON.toString(), iconX - baseX, iconY - baseY, iconSize, iconSize, resolvedIconColor); - scriptModel.divider().set(dividerX - baseX, dividerY - baseY, dividerW, dividerH, - HudRenderUtil.setAlpha(uiMetaColor, 0x52)); - scriptModel.value().set(value, "Onest", drawScale, valueX - baseX, textY - baseY, displayValueWidth, uiValueColor); - scriptModel.unit().set(unit, "OnestMedium", drawScale, unitX - baseX, textY - baseY, unitW, uiMetaColor); - scriptModel.extra().hidden(); - scriptModel.animation().setLabelEffect(labelEffect.get().name(), labelEffectSpeed.get(), time) - .setDigitAnimation(false, "", 1.0f, 0.0f); - scriptModel.data("ping", ping) - .data("valueText", value) - .data("unitText", unit); - ScriptedCompactHudStatRenderer.INSTANCE.render(scriptModel, renderer, fallback, ctx, tickDelta); - } - - @Override - public boolean supportsWidgetAnchoring() { - return true; - } - - @Override - public CompactHudStatModel compactStatModel() { - return scriptModel; - } - - private String getPingValue(int ping) { - if (ping == lastPing) return cachedValue; - lastPing = ping; - cachedValue = ping >= 0 ? Integer.toString(ping) : "--"; - return cachedValue; - } - - private void updatePalette() { - if (isThemeMode()) { - int panelAlpha = bgAlpha.get(); - uiBgPrimary = HudRenderUtil.setAlpha(theme().windowBg(), panelAlpha); - uiBgSecondary = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme().surface(), theme().windowHeader(), 0.35f), - panelAlpha - ); - uiStroke = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme().windowStroke(), theme().strokeSoft(), 0.4f), - Math.min(panelAlpha, 190) - ); - uiIconColor = theme().textPrimary(); - uiValueColor = theme().textPrimary(); - uiMetaColor = theme().textMuted(); - return; - } - - uiBgPrimary = bg.getArgb(); - uiBgSecondary = bg2.getArgb(); - uiStroke = stroke.getArgb(); - uiIconColor = iconColor.getArgb(); - uiValueColor = valueColor.getArgb(); - uiMetaColor = metaColor.getArgb(); - } - - private boolean isThemeMode() { - return COLOR_THEME.equals(colorMode.get()); - } - - private boolean isCustomMode() { - return COLOR_CUSTOM.equals(colorMode.get()); - } - - private boolean isGlassEffect() { - return EFFECT_GLASS.equals(bgEffect.get()); - } - - private boolean hasEffect() { - return !EFFECT_NONE.equals(bgEffect.get()); - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Tps.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Tps.java deleted file mode 100644 index 7c743d3..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Tps.java +++ /dev/null @@ -1,339 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.features.gui.hud.draggable.impl.stats; - -import dev.loki.lovisual.config.values.color.RGBAColorValue; -import dev.loki.lovisual.config.values.color.RGBColorValue; -import dev.loki.lovisual.config.values.mode.EnumValue; -import dev.loki.lovisual.config.values.mode.ModeValue; -import dev.loki.lovisual.config.values.primitive.BooleanValue; -import dev.loki.lovisual.config.values.primitive.NumberValue; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.resources.Identifier; -import net.minecraft.util.Util; -import dev.loki.lovisual.features.gui.hud.registry.HudElementInfo; -import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; -import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; -import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; -import dev.loki.lovisual.features.gui.hud.script.CompactHudStatModel; -import dev.loki.lovisual.features.gui.hud.script.ScriptableHudStatWidget; -import dev.loki.lovisual.features.gui.hud.script.ScriptedCompactHudStatRenderer; -import dev.loki.lovisual.render.engine.animation.AnimationUtility; -import dev.loki.lovisual.render.engine.math.HudScale; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.text.font.info.FontInfo; -import dev.loki.lovisual.render.engine.text.font.Fonts; -import dev.loki.lovisual.render.engine.text.render.TextRenderer; -import dev.loki.lovisual.util.player.interaction.NetworkStatsUtil; - -import static dev.loki.lovisual.features.theme.Theme.theme; - -@HudElementInfo( - id = "tps", - displayName = "TPS", - enabledByDefault = false, - order = 160 -) -public final class Tps extends DraggableHudElement implements ScriptableHudStatWidget { - - private static final Identifier TPS_ICON = Identifier.fromNamespaceAndPath("lovisual", "textures/hud/elements/tps.png"); - private static final float SCALE_MULT = 0.68f; - private static final float BOX_HEIGHT = 20f; - private static final float BOX_RADIUS = 5f; - private static final float BOX_SOFTNESS = 1.0f; - private static final float STROKE_WIDTH = 0.55f; - private static final float ICON_SIZE = 11f; - private static final float ICON_X = 5f; - private static final float DIVIDER_X = 21f; - private static final float DIVIDER_Y = 4f; - private static final float DIVIDER_WIDTH = 0.6f; - private static final float TEXT_LEFT_X = 26f; - private static final float VALUE_UNIT_GAP = 2f; - private static final float EXTRA_GAP = 5f; - private static final float RIGHT_PAD = 5f; - private static final String COLOR_THEME = "Theme"; - private static final String COLOR_CUSTOM = "Custom"; - private static final String EFFECT_NONE = "None"; - private static final String EFFECT_BLUR = "Blur"; - private static final String EFFECT_GLASS = "Glass"; - - private final HudGlobalConfig hud = HudGlobalConfig.get(); - private final BooleanValue extra = bool("tps_extra", false); - private final NumberValue scale = num("tps_scale", 2.37, 0.5, 5.0); - private final ModeValue colorMode = mode("tps_color_mode", "color_mode", "Theme", new String[]{COLOR_THEME, COLOR_CUSTOM}); - private final RGBColorValue iconColor = visibleWhen(colorNoAlpha("tps_icon_color", "#FFFFFF"), this::isCustomMode); - private final RGBColorValue valueColor = visibleWhen(colorNoAlpha("tps_value_color", "#FFFFFF"), this::isCustomMode); - private final RGBColorValue metaColor = visibleWhen(colorNoAlpha("tps_meta_color", "#9B9B9B"), this::isCustomMode); - private final RGBColorValue extraColor = visibleWhen(colorNoAlpha("tps_extra_color", "#BEBEBE"), this::isCustomMode); - private final ModeValue bgEffect = mode("tps_bg_effect", "bg_effect", "Blur", new String[]{EFFECT_NONE, EFFECT_BLUR, EFFECT_GLASS}); - private final RGBAColorValue bg = visibleWhen(color("tps_bg", "#F7343434"), () -> isCustomMode() && !isGlassEffect()); - private final RGBAColorValue bg2 = visibleWhen(color("tps_bg_secondary", "#F7161616"), () -> isCustomMode() && !isGlassEffect()); - private final RGBColorValue stroke = visibleWhen(colorNoAlpha("tps_stroke", "#5A5A5A"), () -> isCustomMode() && !isGlassEffect()); - private final NumberValue bgAlpha = visibleWhen(num("tps_bg_alpha", 133, 0, 255), () -> isThemeMode() && !isGlassEffect()); - private final NumberValue blurAlpha = visibleWhen(num("tps_blur_alpha", 255, 0, 255), this::hasEffect); - private final EnumValue labelEffect = - enumSetting("tps_label_effect", HudTextEffects.Effect.NONE, - HudTextEffects.Effect.NONE, HudTextEffects.Effect.MIX, HudTextEffects.Effect.FLOW, - HudTextEffects.Effect.PULSE, HudTextEffects.Effect.STRIPE); - private final NumberValue labelEffectSpeed = - visibleWhen(num("tps_label_effect_speed", 18, 1, 60), - () -> labelEffect.get() != HudTextEffects.Effect.NONE); - private final CompactHudStatModel scriptModel = new CompactHudStatModel("tps"); - private final StringBuilder tpsBuilder = new StringBuilder(16); - private final StringBuilder extraBuilder = new StringBuilder(16); - private int lastTpsTenth = Integer.MIN_VALUE; - private int lastExtraHundredth = Integer.MIN_VALUE; - private boolean lastExtra; - private String cachedValue = "20.0"; - private String cachedExtra = ""; - private float displayValueWidth = -1f; - private float displayExtraWidth = -1f; - private int uiBgPrimary; - private int uiBgSecondary; - private int uiStroke; - private int uiIconColor; - private int uiValueColor; - private int uiMetaColor; - private int uiExtraColor; - - private static void appendOneDecimal(StringBuilder sb, int tenth) { - if (tenth < 0) { - sb.append('-'); - tenth = -tenth; - } - int intPart = tenth / 10; - int frac = tenth - intPart * 10; - sb.append(intPart).append('.').append((char) ('0' + frac)); - } - - private static void appendTwoDecimals(StringBuilder sb, int hundredth) { - if (hundredth < 0) { - sb.append('-'); - hundredth = -hundredth; - } - int intPart = hundredth / 100; - int frac = hundredth - intPart * 100; - sb.append(intPart) - .append('.') - .append((char) ('0' + (frac / 10))) - .append((char) ('0' + (frac % 10))); - } - - private static float smoothWidth(float current, float target) { - if (current < 0.0f) return target; - if (target >= current) return target; - float next = AnimationUtility.approach(current, target, AnimationUtility.deltaTime(), 12.0f); - return AnimationUtility.snap(next, target, 0.25f); - } - - @Override - public void applyDefaultPosition(int screenW, int screenH) { - this.x = 16f; - this.y = 60f; - } - - @Override - public boolean usesEngineRenderer() { - return true; - } - - @Override - public void renderEngine(Renderer2D renderer, - TextRenderer textRenderer, - GuiGraphicsExtractor ctx, - float tickDelta, - int screenW, - int screenH) { - boolean preview = DraggableHudElementRegistry.isForceVisible(); - if (Minecraft.getInstance() == null && !preview) { - width = 0f; - height = 0f; - scriptModel.setVisible(false); - return; - } - if (!preview && !isEnabled()) { - width = 0f; - height = 0f; - scriptModel.setVisible(false); - return; - } - - Minecraft mc = Minecraft.getInstance(); - float tps = NetworkStatsUtil.getTps(mc); - boolean showExtra = extra.get(); - float clientTps = showExtra ? NetworkStatsUtil.getClientTps(mc) : 0f; - updateTpsStrings(tps, clientTps, showExtra); - updatePalette(); - - TextRenderer fallback = textRenderer != null ? textRenderer : TextRenderer.get(); - TextRenderer valueRenderer = Fonts.renderer("Onest", FontInfo.Type.Regular, fallback); - TextRenderer metaRenderer = Fonts.renderer("OnestMedium", FontInfo.Type.Regular, valueRenderer); - - float drawScale = HudScale.scale(screenW, screenH) - * (hud.getFontSize() / 18f) - * SCALE_MULT - * scale.get().floatValue(); - - valueRenderer.begin(drawScale, true, false); - float valueW = (float) valueRenderer.getWidth(cachedValue, false); - float valueH = (float) valueRenderer.getHeight(false); - valueRenderer.end(); - - metaRenderer.begin(drawScale, true, false); - float unitW = (float) metaRenderer.getWidth("TPS", false); - float unitH = (float) metaRenderer.getHeight(false); - float extraW = showExtra ? (float) metaRenderer.getWidth(cachedExtra, false) : 0f; - metaRenderer.end(); - - float boxH = BOX_HEIGHT * drawScale; - float iconSize = ICON_SIZE * drawScale; - float valueX = x + TEXT_LEFT_X * drawScale; - float unitGap = VALUE_UNIT_GAP * drawScale; - float extraGap = EXTRA_GAP * drawScale; - displayValueWidth = smoothWidth(displayValueWidth, valueW); - displayExtraWidth = showExtra ? smoothWidth(displayExtraWidth, extraW) : 0.0f; - float boxW = (valueX - x) - + displayValueWidth - + unitGap - + unitW - + (showExtra ? extraGap + displayExtraWidth : 0f) - + RIGHT_PAD * drawScale; - width = boxW; - height = boxH; - - float baseX = x; - float baseY = y; - float iconX = baseX + ICON_X * drawScale; - float iconY = baseY + (boxH - iconSize) * 0.5f; - float dividerX = baseX + DIVIDER_X * drawScale; - float dividerY = baseY + DIVIDER_Y * drawScale; - float dividerW = Math.max(0.5f, DIVIDER_WIDTH * drawScale); - float dividerH = Math.max(0.5f, boxH - 2f * DIVIDER_Y * drawScale); - float textY = baseY + (boxH - Math.max(valueH, unitH)) * 0.5f; - float unitX = valueX + displayValueWidth + unitGap; - float extraX = unitX + unitW + extraGap; - - int resolvedIconColor = uiIconColor; - boolean useLabelEffect = labelEffect.get() != HudTextEffects.Effect.NONE; - float time = useLabelEffect ? (float) (Util.getMillis() / 1000.0) : 0.0f; - if (useLabelEffect) { - resolvedIconColor = HudTextEffects.animatedColor(resolvedIconColor, labelEffect.get(), - labelEffectSpeed.get(), time, 0.0f); - } - - float radius = BOX_RADIUS * drawScale; - scriptModel.setVisible(true); - scriptModel.setRoot(baseX, baseY, boxW, boxH, radius, drawScale); - scriptModel.background().set(bgEffect.get(), isThemeMode(), blurAlpha.get() / 255.0f, - uiBgPrimary, uiBgSecondary, uiStroke, Math.max(0.5f, STROKE_WIDTH * drawScale), BOX_SOFTNESS); - scriptModel.icon().texture(TPS_ICON.toString(), iconX - baseX, iconY - baseY, iconSize, iconSize, resolvedIconColor); - scriptModel.divider().set(dividerX - baseX, dividerY - baseY, dividerW, dividerH, - HudRenderUtil.setAlpha(uiMetaColor, 0x52)); - scriptModel.value().set(cachedValue, "Onest", drawScale, valueX - baseX, textY - baseY, displayValueWidth, uiValueColor); - scriptModel.unit().set("TPS", "OnestMedium", drawScale, unitX - baseX, textY - baseY, unitW, uiMetaColor); - if (showExtra) { - scriptModel.extra().set(cachedExtra, "OnestMedium", drawScale, extraX - baseX, textY - baseY, displayExtraWidth, uiExtraColor); - } else { - scriptModel.extra().hidden(); - } - scriptModel.animation().setLabelEffect(labelEffect.get().name(), labelEffectSpeed.get(), time) - .setDigitAnimation(false, "", 1.0f, 0.0f); - scriptModel.data("tps", tps) - .data("clientTps", clientTps) - .data("showExtra", showExtra) - .data("valueText", cachedValue) - .data("unitText", "TPS") - .data("extraText", cachedExtra); - ScriptedCompactHudStatRenderer.INSTANCE.render(scriptModel, renderer, fallback, ctx, tickDelta); - } - - @Override - public boolean supportsWidgetAnchoring() { - return true; - } - - @Override - public CompactHudStatModel compactStatModel() { - return scriptModel; - } - - private void updateTpsStrings(float tps, float extraTps, boolean extraEnabled) { - int tenth = Math.round(tps * 10f); - int hundredth = Math.round(extraTps * 100f); - if (extraEnabled == lastExtra - && tenth == lastTpsTenth - && (!extraEnabled || hundredth == lastExtraHundredth)) { - return; - } - - lastExtra = extraEnabled; - lastTpsTenth = tenth; - lastExtraHundredth = hundredth; - - tpsBuilder.setLength(0); - appendOneDecimal(tpsBuilder, tenth); - cachedValue = tpsBuilder.toString(); - - if (extraEnabled) { - extraBuilder.setLength(0); - extraBuilder.append('['); - appendTwoDecimals(extraBuilder, hundredth); - extraBuilder.append(']'); - cachedExtra = extraBuilder.toString(); - } else { - cachedExtra = ""; - } - } - - private void updatePalette() { - if (isThemeMode()) { - int panelAlpha = bgAlpha.get(); - uiBgPrimary = HudRenderUtil.setAlpha(theme().windowBg(), panelAlpha); - uiBgSecondary = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme().surface(), theme().windowHeader(), 0.35f), - panelAlpha - ); - uiStroke = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme().windowStroke(), theme().strokeSoft(), 0.4f), - Math.min(panelAlpha, 190) - ); - uiIconColor = theme().textPrimary(); - uiValueColor = theme().textPrimary(); - uiMetaColor = theme().textMuted(); - uiExtraColor = HudRenderUtil.mixColor(theme().textMuted(), theme().textPrimary(), 0.18f); - return; - } - - uiBgPrimary = bg.getArgb(); - uiBgSecondary = bg2.getArgb(); - uiStroke = stroke.getArgb(); - uiIconColor = iconColor.getArgb(); - uiValueColor = valueColor.getArgb(); - uiMetaColor = metaColor.getArgb(); - uiExtraColor = extraColor.getArgb(); - } - - private boolean isThemeMode() { - return COLOR_THEME.equals(colorMode.get()); - } - - private boolean isCustomMode() { - return COLOR_CUSTOM.equals(colorMode.get()); - } - - private boolean isGlassEffect() { - return EFFECT_GLASS.equals(bgEffect.get()); - } - - private boolean hasEffect() { - return !EFFECT_NONE.equals(bgEffect.get()); - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Admins.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Admins.java deleted file mode 100644 index 37abd4c..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Admins.java +++ /dev/null @@ -1,708 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.features.gui.hud.draggable.impl.status; - -import dev.loki.lovisual.config.values.color.RGBAColorValue; -import dev.loki.lovisual.config.values.color.RGBColorValue; -import dev.loki.lovisual.config.values.mode.ModeValue; -import dev.loki.lovisual.config.values.primitive.BooleanValue; -import dev.loki.lovisual.config.values.primitive.NumberValue; -import dev.loki.lovisual.util.screen.ClientScreen; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.client.gui.screens.ChatScreen; -import net.minecraft.client.multiplayer.PlayerInfo; -import net.minecraft.client.player.AbstractClientPlayer; -import net.minecraft.network.chat.Component; -import net.minecraft.resources.Identifier; -import net.minecraft.util.Util; -import net.minecraft.world.entity.player.Player; -import dev.loki.lovisual.config.definition.SettingDef; -import dev.loki.lovisual.features.gui.hud.registry.HudElementRegister; -import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; -import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; -import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; -import dev.loki.lovisual.features.relations.StaffData; -import dev.loki.lovisual.features.relations.StaffTracker; -import dev.loki.lovisual.render.engine.animation.AnimationUtility; -import dev.loki.lovisual.render.engine.color.RenderColor; -import dev.loki.lovisual.render.engine.math.HudScale; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.text.font.info.FontInfo; -import dev.loki.lovisual.render.engine.text.font.Fonts; -import dev.loki.lovisual.render.engine.text.render.TextRenderer; -import dev.loki.lovisual.render.helpers.player.PlayerHeadRenderer; -import dev.loki.lovisual.util.player.health.PlayerSkinResolver; -import dev.loki.lovisual.util.text.format.LegacyTextUtil; -import dev.loki.lovisual.util.text.format.TextRenderUtil; -import dev.loki.lovisual.util.text.format.TextRenderUtil.Part; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import static dev.loki.lovisual.features.theme.Theme.theme; -import dev.loki.lovisual.features.gui.hud.draggable.impl.HudPanelLayoutModes; -import dev.loki.lovisual.features.gui.hud.draggable.impl.scripted.ScriptedListHudPanel; - -@HudElementRegister(order = 50) -public final class Admins extends DraggableHudElement { - - { - defaultLinkedLayout(16.0f, 212.0f, "keybinds", "RIGHT", 0.0f, 0.0f); - } - - private static final float HEADER_HEIGHT = 15.5f; - private static final float CONTENT_START_Y = 25.0f; - private static final float ROW_STEP = 11.0f; - private static final float MIN_WIDTH = 96.0f; - private static final float ROW_DIVIDER_X = 15.0f; - private static final float ROW_DIVIDER_H = 6.0f; - private static final float TITLE_TEXT_X = 22.0f; - private static final float COUNT_LABEL_OFFSET = 22.0f; - private static final float ROW_HEAD_X = 3.5f; - private static final float ROW_HEAD_SIZE = 8.0f; - private static final float ROW_TEXT_X = 18.0f; - private static final float ROW_STATUS_RIGHT = 8.0f; - private static final float ROW_CONTENT_CENTER_OFFSET = 1.95f; - private static final String COLOR_THEME = "Theme"; - private static final String COLOR_CUSTOM = "Custom"; - private static final String EFFECT_NONE = "None"; - private static final String EFFECT_BLUR = "Blur"; - - private final HudGlobalConfig hud = HudGlobalConfig.get(); - private final ScriptedListHudPanel scriptedPanel = new ScriptedListHudPanel(); - private final NumberValue scaleValue = - new NumberValue<>("admins_scale", 1.0, HudPanelLayoutModes.SCALE_MIN, HudPanelLayoutModes.SCALE_MAX); - private final ModeValue layoutMode = - new ModeValue("admins_layout", "Split Header", HudPanelLayoutModes.SPLIT_HEADER, HudPanelLayoutModes.UNIFIED_DIVIDER); - private final ModeValue colorMode = - new ModeValue("admins_color_mode", "Theme", COLOR_THEME, COLOR_CUSTOM); - private final ModeValue bgEffect = - new ModeValue("admins_bg_effect", "Blur", EFFECT_NONE, EFFECT_BLUR); - private final RGBAColorValue bg = - new RGBAColorValue("admins_bg", "#EB111318"); - private final RGBAColorValue bg2 = - new RGBAColorValue("admins_bg_secondary", "#F7161616"); - private final NumberValue bgAlpha = - new NumberValue<>("admins_bg_alpha", 131, 0, 255); - private final RGBColorValue stroke = - new RGBColorValue("admins_stroke", "#5A5A5A"); - private final RGBColorValue text = - new RGBColorValue("admins_text", "#FFFFFF"); - private final RGBColorValue muted = - new RGBColorValue("admins_muted", "#A5A5A5"); - private final NumberValue blurAlpha = - new NumberValue<>("admins_blur_alpha", 255, 0, 255); - private final BooleanValue headerIconPulse = - new BooleanValue("admins_header_icon_pulse", true); - private final NumberValue headerIconPulseSpeed = - new NumberValue<>("admins_header_icon_pulse_speed", 18, 1, 60); - private final NumberValue headerIconPulseIntensity = - new NumberValue<>("admins_header_icon_pulse_intensity", 100, 0, 100); - - private final Map entryAnim = new LinkedHashMap<>(); - private final Map lastEntries = new LinkedHashMap<>(); - private float displayWidth = -1.0f; - private float displayHeight = -1.0f; - private float visibilityAnim = 0.0f; - private int uiHeaderLeft; - private int uiHeaderRight; - private int uiBodyLeft; - private int uiBodyRight; - private int uiOutline; - private int uiText; - private int uiMuted; - private int uiCounter; - private int uiTitleText; - private int uiDivider; - private int uiBlurTint; - - public Admins() { - super("admins", "Admins", true); - } - - private static List filterStaff(List in) { - List out = new ArrayList<>(in.size()); - for (StaffData.StaffEntry entry : in) { - if (entry.status() == StaffTracker.Status.OFFLINE && entry.offlineSince() <= 0) continue; - if (entry.status() == StaffTracker.Status.WAITING && entry.lastGone() <= 0) continue; - out.add(entry); - } - return out; - } - - private static int clamp255(int value) { - return Math.max(0, Math.min(255, value)); - } - - @Override - protected void defineSettings(List defs) { - defs.add(SettingDef.number(scaleValue)); - defs.add(SettingDef.mode(layoutMode)); - defs.add(SettingDef.mode(colorMode)); - defs.add(SettingDef.color(bg).visibleWhen(this::isCustomMode)); - defs.add(SettingDef.color(bg2).visibleWhen(this::isCustomMode)); - defs.add(SettingDef.colorNoAlpha(stroke).visibleWhen(this::isCustomMode)); - defs.add(SettingDef.number(bgAlpha).visibleWhen(this::isThemeMode)); - defs.add(SettingDef.colorNoAlpha(text).visibleWhen(this::isCustomMode)); - defs.add(SettingDef.colorNoAlpha(muted).visibleWhen(this::isCustomMode)); - defs.add(SettingDef.mode(bgEffect)); - defs.add(SettingDef.number(blurAlpha).visibleWhen(this::hasEffect)); - defs.add(SettingDef.bool(headerIconPulse)); - defs.add(SettingDef.number(headerIconPulseSpeed).visibleWhen(headerIconPulse::get)); - defs.add(SettingDef.number(headerIconPulseIntensity).visibleWhen(headerIconPulse::get)); - } - - @Override - public void applyDefaultPosition(int screenW, int screenH) { - this.x = screenW - 200.0f; - this.y = 80.0f; - } - - @Override - public boolean usesEngineRenderer() { - return true; - } - - @Override - public void renderEngine(Renderer2D renderer, - TextRenderer textRenderer, - GuiGraphicsExtractor ctx, - float tickDelta, - int screenW, - int screenH) { - boolean forceVisible = DraggableHudElementRegistry.isForceVisible(); - Minecraft mc = Minecraft.getInstance(); - if (mc == null || (mc.player == null && !forceVisible)) { - width = 0.0f; - height = 0.0f; - return; - } - - boolean chatPreview = ClientScreen.current() instanceof ChatScreen; - List entries = filterStaff(StaffData.INSTANCE.getEntries()); - List animatedEntries = animateEntries(entries); - boolean showExampleRow = entries.isEmpty() && (forceVisible || chatPreview); - boolean showWidget = !entries.isEmpty() || showExampleRow; - visibilityAnim = HudRenderUtil.animateVisibility(visibilityAnim, showWidget); - float widgetScale = HudRenderUtil.visibilityScale(visibilityAnim); - if (animatedEntries.isEmpty() && !showExampleRow && visibilityAnim <= 0.0f) { - width = 0.0f; - height = 0.0f; - return; - } - - updatePalette(); - - TextRenderer headerIconRenderer = Fonts.renderer("IconsNur", FontInfo.Type.Regular, TextRenderer.get()); - TextRenderer headerTextRenderer = Fonts.renderer("Inter", FontInfo.Type.Bold, TextRenderer.get()); - TextRenderer rowTextRenderer = Fonts.renderer("OnestMedium", FontInfo.Type.Regular, TextRenderer.get()); - if (headerTextRenderer == null) headerTextRenderer = textRenderer; - if (rowTextRenderer == null) rowTextRenderer = textRenderer; - if (headerIconRenderer == null) headerIconRenderer = textRenderer; - - float baseScale = HudScale.scale(screenW, screenH) * 1.1f * HudPanelLayoutModes.effectiveScale(scaleValue); - float rowStep = ROW_STEP * baseScale; - float fontScale = 0.98f * (hud.getFontSize() / 18.0f); - - headerIconRenderer.begin(fontScale, true, false); - float headerIconH = (float) headerIconRenderer.getHeight(false); - headerIconRenderer.end(); - - headerTextRenderer.begin(fontScale, true, false); - float headerTextH = (float) headerTextRenderer.getHeight(false); - float titleWidth = (float) headerTextRenderer.getWidth("Admins", false); - headerTextRenderer.end(); - - long now = System.currentTimeMillis(); - rowTextRenderer.begin(fontScale, true, false); - float rowTextH = (float) rowTextRenderer.getHeight(false); - float maxWidth = MIN_WIDTH * baseScale; - if (showExampleRow) { - float previewNameWidth = (float) rowTextRenderer.getWidth("Example Staff", false); - float previewStatusWidth = (float) rowTextRenderer.getWidth("Vanish", false); - maxWidth = Math.max(maxWidth, previewNameWidth + previewStatusWidth + (30.0f * baseScale)); - } else { - for (AnimatedEntry animatedEntry : animatedEntries) { - StaffData.StaffEntry entry = animatedEntry.entry(); - float nameWidth = measureLegacyName(rowTextRenderer, entry.displayName()); - float statusWidth = (float) rowTextRenderer.getWidth(formatStatus(entry, now), false); - maxWidth = Math.max(maxWidth, nameWidth + statusWidth + (30.0f * baseScale)); - } - } - rowTextRenderer.end(); - - rowTextRenderer.begin(fontScale * 0.92f, false, false); - String activeCountText = Integer.toString(entries.size()); - float activeLabelWidth = (float) rowTextRenderer.getWidth("Active:", false); - float activeCountWidth = (float) rowTextRenderer.getWidth(activeCountText, false); - rowTextRenderer.end(); - - float headerWidth = (TITLE_TEXT_X * baseScale) + titleWidth - + activeLabelWidth + activeCountWidth - + ((COUNT_LABEL_OFFSET + 12.0f) * baseScale); - maxWidth = Math.max(maxWidth, headerWidth); - - float contentHeight = showExampleRow ? rowStep : totalAnimatedHeight(animatedEntries, rowStep); - float targetHeight = (showExampleRow || !animatedEntries.isEmpty()) - ? (CONTENT_START_Y * baseScale + contentHeight) - : (HEADER_HEIGHT * baseScale); - - displayWidth = HudRenderUtil.animateDimension(displayWidth, maxWidth); - displayHeight = HudRenderUtil.animateDimension(displayHeight, targetHeight); - width = displayWidth; - height = displayHeight; - - if (!showWidget && widgetScale <= 0.001f && animatedEntries.isEmpty()) { - width = 0.0f; - height = 0.0f; - return; - } - - float drawScale = Math.max(0.0f, widgetScale); - float drawWidth = displayWidth * drawScale; - float drawHeight = displayHeight * drawScale; - if (drawWidth <= 0.0f || drawHeight <= 0.0f) { - width = 0.0f; - height = 0.0f; - return; - } - - float drawX = x + ((displayWidth - drawWidth) * 0.5f); - float drawY = y + ((displayHeight - drawHeight) * 0.5f); - float drawBaseScale = baseScale * drawScale; - float drawFontScale = fontScale * drawScale; - float drawHeaderIconH = headerIconH * drawScale; - float drawHeaderTextH = headerTextH * drawScale; - float drawRowTextH = rowTextH * drawScale; - int resolvedHeaderIconColor = uiCounter; - if (headerIconPulse.get()) { - int pulsedHeaderIconColor = HudTextEffects.animatedColor( - uiCounter, - HudTextEffects.Effect.PULSE, - headerIconPulseSpeed.get(), - (float) (Util.getMillis() / 1000.0), - 0.0f - ); - resolvedHeaderIconColor = HudRenderUtil.mixColor( - uiCounter, - pulsedHeaderIconColor, - headerIconPulseIntensity.get() / 100.0f - ); - } - - List> panelRows = ScriptedListHudPanel.rows(); - rowTextRenderer.begin(drawFontScale, false, false); - if (showExampleRow) { - panelRows.add(ScriptedListHudPanel.row( - "preview", - "head", - ScriptedListHudPanel.idString(resolvePreviewSkin()), - List.of(ScriptedListHudPanel.textPart("Example Staff", withAlpha(uiText & 0x00FFFFFF, 255), 0.0f)), - "Vanish", - withAlpha(resolveStatusColor(StaffTracker.Status.VANISH) & 0x00FFFFFF, 255), - withAlpha(uiMuted & 0x00FFFFFF, 130), - 1.0f - )); - } else { - for (AnimatedEntry animatedEntry : animatedEntries) { - float anim = animatedEntry.anim(); - if (anim <= 0.01f) continue; - StaffData.StaffEntry entry = animatedEntry.entry(); - int rowAlpha = clamp255(Math.round(255.0f * anim)); - int nameBaseColor = withAlpha(uiText & 0x00FFFFFF, rowAlpha); - List> parts = legacyParts(rowTextRenderer, entry.displayName(), anim, nameBaseColor); - panelRows.add(ScriptedListHudPanel.row( - entry.name(), - "head", - ScriptedListHudPanel.idString(resolveSkinByName(entry.name())), - parts, - formatStatus(entry, now), - withAlpha(resolveStatusColor(entry.status()) & 0x00FFFFFF, rowAlpha), - withAlpha(uiMuted & 0x00FFFFFF, Math.max(26, rowAlpha - 120)), - anim - )); - } - } - rowTextRenderer.end(); - - scriptedPanel.render( - renderer, - textRenderer, - ctx, - tickDelta, - new ScriptedListHudPanel.Panel( - ScriptedListHudPanel.ADMINS, - new ScriptedListHudPanel.Palette(uiHeaderLeft, uiHeaderRight, uiBodyLeft, uiBodyRight, - uiOutline, uiText, uiMuted, uiCounter, uiTitleText, uiDivider, uiBlurTint), - drawX, - drawY, - drawWidth, - drawHeight, - drawScale, - drawBaseScale, - drawFontScale, - drawHeaderIconH, - drawHeaderTextH, - drawRowTextH, - activeLabelWidth * drawScale, - activeCountWidth * drawScale, - entries.size(), - hasEffect(), - Math.min(1.0f, (blurAlpha.get() / 255.0f) * (isThemeMode() ? 1.15f : 1.0f)), - resolvedHeaderIconColor, - HudPanelLayoutModes.current(layoutMode), - panelRows - ) - ); - } - - @Override - public boolean isDraggable() { - return super.isDraggable(); - } - - private void renderExampleRow(Renderer2D renderer, - TextRenderer rowTextRenderer, - float fontScale, - float rowTextH, - float baseScale, - float renderWidth, - float centerY, - GuiGraphicsExtractor ctx, - float renderX) { - int textColor = withAlpha(uiText & 0x00FFFFFF, 255); - int statusColor = withAlpha(resolveStatusColor(StaffTracker.Status.VANISH) & 0x00FFFFFF, 255); - int dividerColor = withAlpha(uiMuted & 0x00FFFFFF, 130); - float rowContentCenterY = centerY + (ROW_CONTENT_CENTER_OFFSET * baseScale); - - renderHead( - ctx, - resolvePreviewSkin(), - renderX + (ROW_HEAD_X * baseScale), - rowContentCenterY, - ROW_HEAD_SIZE * baseScale, - 255, - baseScale - ); - - renderer.quad( - renderX + (ROW_DIVIDER_X * baseScale), - rowContentCenterY - ((ROW_DIVIDER_H * baseScale) * 0.5f), - Math.max(0.5f, 0.5f * baseScale), - ROW_DIVIDER_H * baseScale, - dividerColor - ); - - rowTextRenderer.begin(fontScale, false, false); - float textY = rowContentCenterY - (rowTextH * 0.5f); - rowTextRenderer.render("Example Staff", - renderX + (ROW_TEXT_X * baseScale), textY, new RenderColor(textColor), false); - float statusWidth = (float) rowTextRenderer.getWidth("Vanish", false); - rowTextRenderer.render("Vanish", - renderX + renderWidth - statusWidth - (ROW_STATUS_RIGHT * baseScale), - textY, - new RenderColor(statusColor), - false - ); - rowTextRenderer.end(); - } - - private void renderHead(GuiGraphicsExtractor ctx, - Identifier skin, - float headX, - float centerY, - float size, - int alpha, - float baseScale) { - float headY = centerY - (size * 0.5f); - float radius = Math.min(1.15f * baseScale, size * 0.24f); - if (skin != null && ctx != null) { - RenderColor color = new RenderColor(0xFFFFFFFF); - color.setAlpha(alpha); - PlayerHeadRenderer.drawRounded( - ctx, - headX, - headY, - size, - radius, - skin, - color, - true, - null, - 0.0f, - false - ); - return; - } - - int fill = withAlpha(uiText & 0x00FFFFFF, Math.max(28, alpha / 5)); - int strokeColor = withAlpha(uiMuted & 0x00FFFFFF, Math.max(80, alpha)); - Renderer2D.COLOR.roundedRect(headX, headY, size, size, radius, 1.0f, fill); - Renderer2D.COLOR.roundedRectStroke( - headX, headY, size, size, radius, 1.0f, - Math.max(0.45f, 0.55f * baseScale), - strokeColor - ); - } - - private Identifier resolvePreviewSkin() { - Minecraft mc = Minecraft.getInstance(); - if (mc != null && mc.player != null) { - return PlayerSkinResolver.resolveProfileSkin(mc.player.getGameProfile()); - } - return null; - } - - private void updatePalette() { - if (isThemeMode()) { - int alpha = bgAlpha.get(); - int window = HudRenderUtil.setAlpha(theme().windowBg(), alpha); - int header = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme().windowHeader(), theme().surface(), 0.18f), - Math.min(255, alpha + 14) - ); - int surface = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme().surface(), theme().windowBg(), 0.22f), - Math.min(255, alpha + 6) - ); - int deep = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme().surface(), theme().windowHeader(), 0.42f), - Math.min(255, alpha + 18) - ); - - uiHeaderLeft = HudRenderUtil.mixColor(header, window, 0.22f); - uiHeaderRight = HudRenderUtil.mixColor(surface, header, 0.52f); - uiBodyLeft = HudRenderUtil.mixColor(window, surface, 0.24f); - uiBodyRight = HudRenderUtil.mixColor(deep, surface, 0.18f); - uiOutline = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme().windowStroke(), theme().strokeSoft(), 0.18f), - Math.min(255, Math.max(182, alpha + 36)) - ); - uiText = theme().textPrimary(); - uiTitleText = HudRenderUtil.mixColor(theme().textPrimary(), theme().accent(), 0.14f); - uiMuted = HudRenderUtil.mixColor(theme().textMuted(), theme().textPrimary(), 0.16f); - uiCounter = HudRenderUtil.mixColor(theme().accent(), theme().textPrimary(), 0.38f); - uiDivider = HudRenderUtil.scaleAlpha(uiMuted, 0.68f); - uiBlurTint = HudRenderUtil.mixColor( - HudRenderUtil.mixColor(uiHeaderRight, uiBodyRight, 0.5f), - theme().accent(), - 0.14f - ); - return; - } - - uiHeaderLeft = bg.getArgb(); - uiHeaderRight = bg2.getArgb(); - uiBodyLeft = HudRenderUtil.mixColor(bg.getArgb(), bg2.getArgb(), 0.15f); - uiBodyRight = bg2.getArgb(); - uiOutline = stroke.getArgb(); - uiText = text.getArgb(); - uiTitleText = uiText; - uiMuted = muted.getArgb(); - uiCounter = HudRenderUtil.mixColor(text.getArgb(), 0xFFE1E1FF, 0.20f); - uiDivider = HudRenderUtil.scaleAlpha(uiMuted, 0.55f); - uiBlurTint = HudRenderUtil.mixColor(uiHeaderLeft, uiBodyRight, 0.5f); - } - - private List animateEntries(List entries) { - Map current = new LinkedHashMap<>(); - for (StaffData.StaffEntry entry : entries) { - current.put(entry.name(), entry); - } - - Map merged = new LinkedHashMap<>(); - merged.putAll(current); - for (Map.Entry entry : lastEntries.entrySet()) { - merged.putIfAbsent(entry.getKey(), entry.getValue()); - } - lastEntries.clear(); - lastEntries.putAll(merged); - - List toRemove = new ArrayList<>(); - for (Map.Entry entry : lastEntries.entrySet()) { - String key = entry.getKey(); - float target = current.containsKey(key) ? 1.0f : 0.0f; - float anim = entryAnim.getOrDefault(key, 0.0f); - anim = AnimationUtility.approach(anim, target, AnimationUtility.deltaTime(), 12.0f); - anim = AnimationUtility.snap(anim, target, 0.02f); - if (anim <= 0.0f && target <= 0.0f) { - toRemove.add(key); - } else { - entryAnim.put(key, anim); - } - } - for (String key : toRemove) { - entryAnim.remove(key); - lastEntries.remove(key); - } - - List out = new ArrayList<>(); - for (Map.Entry entry : lastEntries.entrySet()) { - float anim = entryAnim.getOrDefault(entry.getKey(), 0.0f); - if (anim > 0.01f) { - out.add(new AnimatedEntry(entry.getValue(), anim)); - } - } - return out; - } - - private float totalAnimatedHeight(List rows, float rowStep) { - float out = 0.0f; - for (AnimatedEntry row : rows) { - out += rowStep * row.anim(); - } - return out; - } - - private float measureLegacyName(TextRenderer renderer, Component displayName) { - Component converted = LegacyTextUtil.convertLegacyCodes(displayName); - List parts = TextRenderUtil.flattenStyled(converted, theme().textPrimary()); - float out = 0.0f; - for (Part part : parts) { - out += (float) renderer.getWidth(part.text(), false); - } - return out; - } - - private void drawLegacyName(TextRenderer renderer, - float fontScale, - float x, - float y, - Component displayName, - float alpha, - int fallbackColor) { - Component converted = LegacyTextUtil.convertLegacyCodes(displayName); - List parts = TextRenderUtil.flattenStyled(converted, fallbackColor); - - renderer.begin(fontScale, false, false); - float cursorX = x; - for (Part part : parts) { - renderer.render( - part.text(), - cursorX, - y, - new RenderColor(HudRenderUtil.scaleAlpha(part.color(), alpha)), - false - ); - cursorX += (float) renderer.getWidth(part.text(), false); - } - renderer.end(); - } - - private List> legacyParts(TextRenderer renderer, - Component displayName, - float alpha, - int fallbackColor) { - Component converted = LegacyTextUtil.convertLegacyCodes(displayName); - List parts = TextRenderUtil.flattenStyled(converted, fallbackColor); - List> out = new ArrayList<>(parts.size()); - float cursorX = 0.0f; - for (Part part : parts) { - int color = HudRenderUtil.scaleAlpha(part.color(), alpha); - out.add(ScriptedListHudPanel.textPart(part.text(), color, cursorX)); - cursorX += (float) renderer.getWidth(part.text(), false); - } - return out; - } - - private AbstractClientPlayer resolvePlayerByName(String name) { - Minecraft mc = Minecraft.getInstance(); - if (name == null || name.isEmpty() || mc == null || mc.level == null) { - return null; - } - for (Player player : mc.level.players()) { - if (player instanceof AbstractClientPlayer clientPlayer) { - String playerName = clientPlayer.getGameProfile() != null - ? clientPlayer.getGameProfile().name() - : clientPlayer.getName().getString(); - if (playerName != null && playerName.equalsIgnoreCase(name)) { - return clientPlayer; - } - } - } - return null; - } - - private Identifier resolveSkinByName(String name) { - AbstractClientPlayer player = resolvePlayerByName(name); - if (player != null) { - return PlayerSkinResolver.resolveProfileSkin(player.getGameProfile()); - } - - Minecraft mc = Minecraft.getInstance(); - if (mc != null && mc.getConnection() != null) { - for (PlayerInfo entry : mc.getConnection().getOnlinePlayers()) { - var profile = entry.getProfile(); - if (profile == null || profile.name() == null) continue; - if (!profile.name().equalsIgnoreCase(name)) continue; - Identifier skin = PlayerSkinResolver.resolveProfileSkin(profile); - if (skin != null) return skin; - } - } - return null; - } - - private int resolveStatusColor(StaffTracker.Status status) { - return switch (status) { - case GM0 -> 0xFF55FF55; - case GM1 -> 0xFF55DDFF; - case GM2 -> 0xFFFFFF55; - case GM3 -> 0xFFFF55FF; - case VANISH -> 0xFFFF7755; - case WAITING -> 0xFFAAAAAA; - case OFFLINE -> 0xFF888888; - default -> uiMuted != 0 ? uiMuted : 0xFF888888; - }; - } - - private String formatStatus(StaffData.StaffEntry entry, long now) { - return switch (entry.status()) { - case OFFLINE -> entry.offlineSince() <= 0 ? "--:--" : formatTime(now - entry.offlineSince()); - case WAITING -> entry.lastGone() <= 0 ? "--:--" : formatTime(now - entry.lastGone()); - case VANISH -> "Vanish"; - default -> entry.status().name(); - }; - } - - private String formatTime(long ms) { - if (ms < 0) { - return "--:--"; - } - long sec = ms / 1000L; - long min = sec / 60L; - long hrs = min / 60L; - if (hrs == 0) { - return String.format("%02d:%02d", min, sec % 60L); - } - return String.format("%02d:%02d:%02d", hrs, min % 60L, sec % 60L); - } - - private int withAlpha(int rgb, int alpha) { - return (clamp255(alpha) << 24) | (rgb & 0x00FFFFFF); - } - - private boolean isThemeMode() { - return COLOR_THEME.equals(colorMode.get()); - } - - private boolean isCustomMode() { - return COLOR_CUSTOM.equals(colorMode.get()); - } - - private boolean hasEffect() { - return !EFFECT_NONE.equals(bgEffect.get()); - } - - private record AnimatedEntry(StaffData.StaffEntry entry, float anim) { - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Armor.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Armor.java deleted file mode 100644 index 46ae5e1..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Armor.java +++ /dev/null @@ -1,324 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.features.gui.hud.draggable.impl.status; - -import dev.loki.lovisual.config.values.mode.EnumValue; -import dev.loki.lovisual.config.values.primitive.BooleanValue; -import dev.loki.lovisual.config.values.primitive.NumberValue; -import dev.loki.lovisual.util.screen.ClientScreen; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.client.gui.screens.ChatScreen; -import net.minecraft.world.entity.EquipmentSlot; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.Items; -import dev.loki.lovisual.features.gui.hud.registry.HudElementInfo; -import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; -import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; -import dev.loki.lovisual.render.engine.math.HudScale; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.text.render.TextRenderer; - -//todo Description -@HudElementInfo( - id = "armor", - displayName = "Armor", - enabledByDefault = false, - order = 210 -) -public final class Armor extends DraggableHudElement { - - { - defaultLayout(1616.315f, 1003.6977f); - } - - private static final int SLOTS = 4; - private static final float DEFAULT_SLOT_SIZE = 18f; - private static final float DEFAULT_SLOT_GAP = 2f; - private static final float PANEL_RADIUS = 4.0f; - private static final float GLASS_BLUR_STRENGTH = 1.0f; - private static final float GLASS_ALPHA = 1.0f; - private static final float DEFAULT_MARGIN = 16f; - private static final float DIVIDER_THICKNESS = 0.5f; - - private final HudGlobalConfig hud = HudGlobalConfig.get(); - private final NumberValue scaleValue = num("armor_scale", 3.68, 0.5, 5.0); - private final EnumValue layoutMode = - enumSetting("armor_layout", LayoutMode.HORIZONTAL, LayoutMode.VERTICAL, LayoutMode.HORIZONTAL); - private final BooleanValue reverse = bool("armor_reverse", false); - private final EnumValue stateMode = - enumSetting("armor_state_mode", StateMode.TEXT, StateMode.BAR, StateMode.TEXT); - private final NumberValue stateBarThreshold = - visibleWhen(num("armor_state_bar_threshold", 70, 0, 100), () -> stateMode.get() == StateMode.BAR); - private final NumberValue stateThreshold = - visibleWhen(num("armor_state_threshold", 70, 0, 100), () -> stateMode.get() == StateMode.TEXT); - private final NumberValue stateColorThreshold = - visibleWhen(num("armor_state_color_threshold", 70, 0, 100), () -> stateMode.get() == StateMode.TEXT); - private float baseX; - private float baseY; - private float slotSize; - private float slotGap; - private float slotRadius; - private float itemScale; - private boolean layoutReady; - private ItemStack[] cachedStacks; - private float renderX; - private float renderY; - - private static boolean hasAnyStacks(ItemStack[] stacks) { - if (stacks == null) return false; - for (ItemStack stack : stacks) { - if (stack != null && !stack.isEmpty()) { - return true; - } - } - return false; - } - - @Override - public void applyDefaultPosition(int screenW, int screenH) { - float scale = HudScale.scale(screenW, screenH) * scaleValue.get().floatValue(); - float size = DEFAULT_SLOT_SIZE * scale; - float gap = DEFAULT_SLOT_GAP * scale; - boolean horizontal = isHorizontal(); - float w = horizontal ? SLOTS * size + (SLOTS - 1) * gap : size; - float h = horizontal ? size : SLOTS * size + (SLOTS - 1) * gap; - if (horizontal) { - this.x = screenW * 0.5f - w * 0.5f; - this.y = screenH - h - DEFAULT_MARGIN * scale; - } else { - this.x = screenW - w - DEFAULT_MARGIN * scale; - this.y = screenH * 0.5f - h * 0.5f; - } - } - - @Override - public boolean usesEngineRenderer() { - return true; - } - - @Override - public void renderEngine(Renderer2D renderer, - TextRenderer textRenderer, - GuiGraphicsExtractor ctx, - float tickDelta, - int screenW, - int screenH) { - Minecraft mc = Minecraft.getInstance(); - layoutReady = false; - boolean preview = DraggableHudElementRegistry.isForceVisible(); - float scale = HudScale.scale(screenW, screenH) * scaleValue.get().floatValue(); - boolean horizontal = isHorizontal(); - - slotSize = DEFAULT_SLOT_SIZE * scale; - slotGap = DEFAULT_SLOT_GAP * scale; - slotRadius = PANEL_RADIUS * scale; - itemScale = scale; - - width = horizontal ? SLOTS * slotSize + (SLOTS - 1) * slotGap : slotSize; - height = horizontal ? slotSize : SLOTS * slotSize + (SLOTS - 1) * slotGap; - - if (mc == null || (mc.player == null && !preview)) { - width = 0f; - height = 0f; - return; - } - if (!preview && !isEnabled()) { - width = 0f; - height = 0f; - return; - } - - ItemStack[] stacks = (mc != null && mc.player != null) ? getArmorStacks() : new ItemStack[0]; - if (preview && !hasAnyStacks(stacks)) { - stacks = previewStacks(); - } - cachedStacks = stacks; - if (!hasAnyStacks(cachedStacks) && !shouldShowWhenEmpty() && !preview) { - width = 0f; - height = 0f; - return; - } - - boolean reverse = x > screenW * 0.5f; - renderX = reverse ? (x - width) : x; - renderY = y; - baseX = renderX; - baseY = renderY; - - float glassScale = PANEL_RADIUS <= 0.0f ? 1.0f : slotRadius / PANEL_RADIUS; - HudRenderUtil.drawLiquidGlass( - baseX, - baseY, - width, - height, - slotRadius, - glassScale, - true, - GLASS_BLUR_STRENGTH, - GLASS_ALPHA - ); - renderer.roundedRectCorners( - baseX, - baseY, - width, - height, - slotRadius, - slotRadius, - slotRadius, - slotRadius, - 1.0f, - HudRenderUtil.glassSmallBackground(GLASS_ALPHA) - ); - drawSlotDividers(renderer, horizontal, scale); - - layoutReady = true; - } - - @Override - public void renderEngineForeground(Renderer2D renderer, - TextRenderer textRenderer, - GuiGraphicsExtractor ctx, - float tickDelta, - int screenW, - int screenH) { - Minecraft mc = Minecraft.getInstance(); - if (!layoutReady || mc == null || mc.player == null || ctx == null) return; - - ItemStack[] stacks = cachedStacks != null ? cachedStacks : getArmorStacks(); - float iconSize = 16f * itemScale; - float offset = (slotSize - iconSize) * 0.5f; - boolean horizontal = isHorizontal(); - boolean reverseOrder = isReverseOrder(); - StateMode mode = stateMode.get(); - int overlayFlags = mode == StateMode.TEXT - ? Renderer2D.ITEM_OVERLAY_DURABILITY_TEXT - : Renderer2D.ITEM_OVERLAY_DURABILITY; - int durabilityThreshold = mode == StateMode.TEXT ? stateThreshold.get() : stateBarThreshold.get(); - int durabilityColorThreshold = mode == StateMode.TEXT ? stateColorThreshold.get() : stateBarThreshold.get(); - - int seed = 0; - for (int i = 0; i < stacks.length; i++) { - int idx = reverseOrder ? (SLOTS - 1 - i) : i; - ItemStack stack = stacks[idx]; - if (stack == null || stack.isEmpty()) continue; - float sx = baseX + (horizontal ? i * (slotSize + slotGap) : 0f) + offset; - float sy = baseY + (horizontal ? 0f : i * (slotSize + slotGap)) + offset; - - // Armor widget is laid out in UNSCALED_LOGICAL coordinates together with its slots. - renderer.item(stack, sx, sy, itemScale, seed++, overlayFlags, null, - durabilityThreshold, durabilityColorThreshold); - } - } - - private ItemStack[] getArmorStacks() { - Minecraft mc = Minecraft.getInstance(); - return new ItemStack[]{ - mc.player.getItemBySlot(EquipmentSlot.HEAD), - mc.player.getItemBySlot(EquipmentSlot.CHEST), - mc.player.getItemBySlot(EquipmentSlot.LEGS), - mc.player.getItemBySlot(EquipmentSlot.FEET) - }; - } - - private ItemStack[] previewStacks() { - ItemStack head = new ItemStack(Items.DIAMOND_HELMET); - ItemStack chest = new ItemStack(Items.DIAMOND_CHESTPLATE); - ItemStack legs = new ItemStack(Items.DIAMOND_LEGGINGS); - ItemStack feet = new ItemStack(Items.DIAMOND_BOOTS); - head.setDamageValue(Math.max(1, head.getMaxDamage() / 3)); - chest.setDamageValue(Math.max(1, chest.getMaxDamage() / 2)); - legs.setDamageValue(Math.max(1, legs.getMaxDamage() / 4)); - feet.setDamageValue(Math.max(1, feet.getMaxDamage() / 5)); - return new ItemStack[]{head, chest, legs, feet}; - } - - private void drawSlotDividers(Renderer2D renderer, boolean horizontal, float scale) { - if (renderer == null) return; - float dividerThickness = Math.max(0.5f, DIVIDER_THICKNESS * scale); - float dividerLength = Math.max(6.0f * scale, slotSize * 0.52f); - float dividerInset = Math.max(0.0f, (slotSize - dividerLength) * 0.5f); - int divider = HudRenderUtil.scaleAlpha(0xFFFFFFFF, 0.12f); - int dividerShadow = HudRenderUtil.scaleAlpha(0xFF000000, 0.10f); - for (int i = 0; i < SLOTS - 1; i++) { - if (horizontal) { - float lineX = baseX + slotSize + i * (slotSize + slotGap) - + slotGap * 0.5f - dividerThickness * 0.5f; - float lineY = baseY + dividerInset; - renderer.quad(lineX + dividerThickness, lineY, dividerThickness, dividerLength, dividerShadow); - renderer.quad(lineX, lineY, dividerThickness, dividerLength, divider); - } else { - float lineX = baseX + dividerInset; - float lineY = baseY + slotSize + i * (slotSize + slotGap) - + slotGap * 0.5f - dividerThickness * 0.5f; - renderer.quad(lineX, lineY + dividerThickness, dividerLength, dividerThickness, dividerShadow); - renderer.quad(lineX, lineY, dividerLength, dividerThickness, divider); - } - } - } - - private boolean shouldShowWhenEmpty() { - Minecraft mc = Minecraft.getInstance(); - if (hud == null) return false; - HudGlobalConfig.HeaderMode mode = hud.getHeaderMode(); - if (mode == HudGlobalConfig.HeaderMode.ALWAYS) return true; - if (mode == HudGlobalConfig.HeaderMode.CHAT) { - return mc != null && ClientScreen.current() instanceof ChatScreen; - } - return false; - } - - private boolean isHorizontal() { - return layoutMode.get() == LayoutMode.HORIZONTAL; - } - - private boolean isReverseOrder() { - return reverse.get(); - } - - @Override - public boolean contains(float mx, float my) { - return mx >= renderX && mx <= renderX + width && my >= renderY && my <= renderY + height; - } - - private enum LayoutMode implements EnumValue.IdProvider { - VERTICAL("Vertical"), - HORIZONTAL("Horizontal"); - - private final String id; - - LayoutMode(String id) { - this.id = id; - } - - @Override - public String id() { - return id; - } - } - - private enum StateMode implements EnumValue.IdProvider { - BAR("Bar"), - TEXT("Text"); - - private final String id; - - StateMode(String id) { - this.id = id; - } - - @Override - public String id() { - return id; - } - } - -} - diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Cooldowns.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Cooldowns.java deleted file mode 100644 index d137bb4..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Cooldowns.java +++ /dev/null @@ -1,707 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.features.gui.hud.draggable.impl.status; - -import dev.loki.lovisual.config.values.color.RGBAColorValue; -import dev.loki.lovisual.config.values.color.RGBColorValue; -import dev.loki.lovisual.config.values.mode.ModeValue; -import dev.loki.lovisual.config.values.primitive.BooleanValue; -import dev.loki.lovisual.config.values.primitive.NumberValue; -import dev.loki.lovisual.util.screen.ClientScreen; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.client.gui.screens.ChatScreen; -import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.resources.Identifier; -import net.minecraft.util.Util; -import net.minecraft.world.item.Item; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.Items; -import dev.loki.lovisual.config.definition.SettingDef; -import dev.loki.lovisual.features.gui.hud.registry.HudElementRegister; -import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; -import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; -import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; -import dev.loki.lovisual.features.module.lifecycle.Modules; -import dev.loki.lovisual.features.module.modules.combat.pvp.PvpCooldowns; -import dev.loki.lovisual.mixins.accessors.inventory.ItemCooldownEntryAccessor; -import dev.loki.lovisual.mixins.accessors.inventory.ItemCooldownManagerAccessor; -import dev.loki.lovisual.render.engine.animation.AnimationUtility; -import dev.loki.lovisual.render.engine.color.RenderColor; -import dev.loki.lovisual.render.engine.math.HudScale; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.text.font.info.FontInfo; -import dev.loki.lovisual.render.engine.text.font.Fonts; -import dev.loki.lovisual.render.engine.text.render.TextRenderer; -import dev.loki.lovisual.util.pvp.cooldown.CooldownRegistry; -import dev.loki.lovisual.util.pvp.cooldown.ItemCooldownSnapshot; -import dev.loki.lovisual.util.pvp.state.PvpState; -import dev.loki.lovisual.util.pvp.client.CooldownsState; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import static dev.loki.lovisual.features.theme.Theme.theme; -import dev.loki.lovisual.features.gui.hud.draggable.impl.HudPanelLayoutModes; -import dev.loki.lovisual.features.gui.hud.draggable.impl.scripted.ScriptedListHudPanel; - -@HudElementRegister(order = 30) -public final class Cooldowns extends DraggableHudElement { - - { - defaultLayout(1160.4823f, 466.0f); - } - - private static final float HEADER_HEIGHT = 15.5f; - private static final float BODY_Y_OFFSET = 18.5f; - private static final float CONTENT_START_Y = 25.0f; - private static final float BODY_INSET_Y = 6.5f; - private static final float ROW_STEP = 11.0f; - private static final float MIN_WIDTH = 110.0f; - private static final float PANEL_RADIUS = 4.0f; - private static final float PANEL_SOFTNESS = 1.0f; - private static final float PANEL_STROKE = 0.55f; - private static final float HEADER_DIVIDER_X = 18.0f; - private static final float HEADER_DIVIDER_Y = 5.0f; - private static final float HEADER_DIVIDER_H = 6.0f; - private static final float ROW_DIVIDER_X = 15.0f; - private static final float ROW_DIVIDER_H = 6.0f; - private static final float TITLE_ICON_X = 4.0f; - private static final float TITLE_TEXT_X = 22.0f; - private static final float COUNT_LABEL_OFFSET = 22.0f; - private static final float COUNT_VALUE_OFFSET = 2.5f; - private static final float ROW_ICON_X = 3.5f; - private static final float ROW_ICON_VISUAL_SIZE = 8.0f; - private static final float ROW_TEXT_X = 18.0f; - private static final float ROW_TIME_RIGHT = 8.0f; - private static final float ROW_CONTENT_CENTER_OFFSET = 1.9f; - private static final int BLINK_THRESHOLD_TICKS = 100; - private static final double BLINK_SPEED = 0.008; - private static final String COLOR_THEME = "Theme"; - private static final String COLOR_CUSTOM = "Custom"; - private static final String EFFECT_NONE = "None"; - private static final String EFFECT_BLUR = "Blur"; - private static final int PVP_ACTIVE_COLOR = 0xFFFFA500; - private static final int PVP_IDLE_COLOR = 0xFF00A0FF; - private static final int PVP_BLOCKED_COLOR = 0xFFFF5656; - private static final Item[] PREVIEW_ITEMS = new Item[]{ - Items.ENDER_EYE, Items.ENDER_PEARL, Items.SUGAR, Items.MACE, - Items.ENCHANTED_GOLDEN_APPLE, Items.TRIDENT, Items.CROSSBOW, - Items.DRIED_KELP, Items.NETHERITE_SCRAP - }; - private static ItemStack[] previewStacks; - - private final HudGlobalConfig hud = HudGlobalConfig.get(); - private final ScriptedListHudPanel scriptedPanel = new ScriptedListHudPanel(); - private final NumberValue scaleValue = - new NumberValue<>("cooldowns_scale", 1.0, HudPanelLayoutModes.SCALE_MIN, HudPanelLayoutModes.SCALE_MAX); - private final ModeValue layoutMode = - new ModeValue("cooldowns_layout", "Split Header", HudPanelLayoutModes.SPLIT_HEADER, HudPanelLayoutModes.UNIFIED_DIVIDER); - private final ModeValue colorMode = - new ModeValue("cooldowns_color_mode", "Theme", COLOR_THEME, COLOR_CUSTOM); - private final ModeValue bgEffect = - new ModeValue("cooldowns_bg_effect", "Blur", EFFECT_NONE, EFFECT_BLUR); - private final BooleanValue blink = - new BooleanValue("cooldowns_blink", true); - private final RGBAColorValue bg = - new RGBAColorValue("cooldowns_bg", "#EB111318"); - private final RGBAColorValue bg2 = - new RGBAColorValue("cooldowns_bg_secondary", "#F7161616"); - private final NumberValue bgAlpha = - new NumberValue<>("cooldowns_bg_alpha", 185, 0, 255); - private final RGBColorValue stroke = - new RGBColorValue("cooldowns_stroke", "#5A5A5A"); - private final RGBColorValue text = - new RGBColorValue("cooldowns_text", "#FFFFFF"); - private final RGBColorValue muted = - new RGBColorValue("cooldowns_muted", "#A5A5A5"); - private final NumberValue blurAlpha = - new NumberValue<>("cooldowns_blur_alpha", 255, 0, 255); - private final BooleanValue headerIconPulse = - new BooleanValue("cooldowns_header_icon_pulse", true); - private final NumberValue headerIconPulseSpeed = - new NumberValue<>("cooldowns_header_icon_pulse_speed", 18, 1, 60); - private final NumberValue headerIconPulseIntensity = - new NumberValue<>("cooldowns_header_icon_pulse_intensity", 100, 0, 100); - - private final Map entryAnim = new LinkedHashMap<>(); - private final Map lastEntries = new LinkedHashMap<>(); - private final List pendingItemTasks = new ArrayList<>(9); - private float displayWidth = -1.0f; - private float displayHeight = -1.0f; - private float visibilityAnim = 0.0f; - private boolean foregroundReady; - private int uiHeaderLeft; - private int uiHeaderRight; - private int uiBodyLeft; - private int uiBodyRight; - private int uiOutline; - private int uiText; - private int uiMuted; - private int uiCounter; - private int uiTitleText; - private int uiDivider; - private int uiBlurTint; - - public Cooldowns() { - super("cooldowns", "Cooldowns", true); - } - - private static int clamp255(int value) { - return Math.max(0, Math.min(255, value)); - } - - private static ItemStack[] previewStacks() { - if (previewStacks == null || previewStacks.length != PREVIEW_ITEMS.length) { - previewStacks = buildPreviewStacks(); - } - return previewStacks; - } - - private static ItemStack[] buildPreviewStacks() { - ItemStack[] stacks = new ItemStack[PREVIEW_ITEMS.length]; - for (int i = 0; i < PREVIEW_ITEMS.length; i++) { - stacks[i] = new ItemStack(PREVIEW_ITEMS[i]); - } - return stacks; - } - - @Override - protected void defineSettings(List defs) { - defs.add(SettingDef.number(scaleValue)); - defs.add(SettingDef.mode(layoutMode)); - defs.add(SettingDef.mode(colorMode)); - defs.add(SettingDef.color(bg).visibleWhen(this::isCustomMode)); - defs.add(SettingDef.color(bg2).visibleWhen(this::isCustomMode)); - defs.add(SettingDef.colorNoAlpha(stroke).visibleWhen(this::isCustomMode)); - defs.add(SettingDef.number(bgAlpha).visibleWhen(this::isThemeMode)); - defs.add(SettingDef.colorNoAlpha(text).visibleWhen(this::isCustomMode)); - defs.add(SettingDef.colorNoAlpha(muted).visibleWhen(this::isCustomMode)); - defs.add(SettingDef.mode(bgEffect)); - defs.add(SettingDef.number(blurAlpha).visibleWhen(this::hasEffect)); - defs.add(SettingDef.bool(blink)); - defs.add(SettingDef.bool(headerIconPulse)); - defs.add(SettingDef.number(headerIconPulseSpeed).visibleWhen(headerIconPulse::get)); - defs.add(SettingDef.number(headerIconPulseIntensity).visibleWhen(headerIconPulse::get)); - } - - @Override - public void applyDefaultPosition(int screenW, int screenH) { - this.x = screenW - 132.0f; - this.y = 20.0f; - } - - @Override - public boolean usesEngineRenderer() { - return true; - } - - @Override - public void renderEngine(Renderer2D renderer, - TextRenderer textRenderer, - GuiGraphicsExtractor ctx, - float tickDelta, - int screenW, - int screenH) { - Minecraft mc = Minecraft.getInstance(); - foregroundReady = false; - pendingItemTasks.clear(); - boolean forceVisible = DraggableHudElementRegistry.isForceVisible(); - if (mc == null || (mc.player == null && !forceVisible)) { - width = 0.0f; - height = 0.0f; - return; - } - - boolean chatPreview = ClientScreen.current() instanceof ChatScreen; - List rows = collectRows(); - List animatedRows = animateRows(rows); - boolean showExampleRow = rows.isEmpty() && (forceVisible || chatPreview); - boolean showWidget = !rows.isEmpty() || showExampleRow; - visibilityAnim = HudRenderUtil.animateVisibility(visibilityAnim, showWidget); - float widgetScale = HudRenderUtil.visibilityScale(visibilityAnim); - if (animatedRows.isEmpty() && !showExampleRow && visibilityAnim <= 0.0f) { - width = 0.0f; - height = 0.0f; - return; - } - - updatePalette(); - - TextRenderer headerIconRenderer = Fonts.renderer("IconsNur", FontInfo.Type.Regular, TextRenderer.get()); - TextRenderer headerTextRenderer = Fonts.renderer("Inter", FontInfo.Type.Bold, TextRenderer.get()); - TextRenderer rowTextRenderer = Fonts.renderer("OnestMedium", FontInfo.Type.Regular, TextRenderer.get()); - if (headerTextRenderer == null) headerTextRenderer = textRenderer; - if (rowTextRenderer == null) rowTextRenderer = textRenderer; - if (headerIconRenderer == null) headerIconRenderer = textRenderer; - - float baseScale = HudScale.scale(screenW, screenH) * 1.1f * HudPanelLayoutModes.effectiveScale(scaleValue); - float rowStep = ROW_STEP * baseScale; - float fontScale = 0.98f * (hud.getFontSize() / 18.0f); - - headerIconRenderer.begin(fontScale, true, false); - float headerIconH = (float) headerIconRenderer.getHeight(false); - headerIconRenderer.end(); - - headerTextRenderer.begin(fontScale, true, false); - float headerTextH = (float) headerTextRenderer.getHeight(false); - float titleWidth = (float) headerTextRenderer.getWidth("Cooldowns", false); - headerTextRenderer.end(); - - rowTextRenderer.begin(fontScale, true, false); - float rowTextH = (float) rowTextRenderer.getHeight(false); - float maxWidth = MIN_WIDTH * baseScale; - if (showExampleRow) { - float previewWidth = (float) rowTextRenderer.getWidth("Example Cooldowns**:**", false) + (30.0f * baseScale); - maxWidth = Math.max(maxWidth, previewWidth); - } else { - for (AnimatedRow animatedRow : animatedRows) { - Row row = animatedRow.row(); - float widthCandidate = (float) rowTextRenderer.getWidth(row.name() + row.duration(), false) + (30.0f * baseScale); - maxWidth = Math.max(maxWidth, widthCandidate); - } - } - rowTextRenderer.end(); - - rowTextRenderer.begin(fontScale * 0.92f, false, false); - String activeCountText = Integer.toString(rows.size()); - float activeLabelWidth = (float) rowTextRenderer.getWidth("Active:", false); - float activeCountWidth = (float) rowTextRenderer.getWidth(activeCountText, false); - rowTextRenderer.end(); - - float headerWidth = (TITLE_TEXT_X * baseScale) + titleWidth - + activeLabelWidth + activeCountWidth - + ((COUNT_LABEL_OFFSET + 12.0f) * baseScale); - maxWidth = Math.max(maxWidth, headerWidth); - - float contentHeight = showExampleRow ? rowStep : totalAnimatedHeight(animatedRows, rowStep); - float targetHeight = (showExampleRow || !animatedRows.isEmpty()) - ? (CONTENT_START_Y * baseScale + contentHeight) - : (HEADER_HEIGHT * baseScale); - - displayWidth = HudRenderUtil.animateDimension(displayWidth, maxWidth); - displayHeight = HudRenderUtil.animateDimension(displayHeight, targetHeight); - width = displayWidth; - height = displayHeight; - - if (!showWidget && widgetScale <= 0.001f && animatedRows.isEmpty()) { - width = 0.0f; - height = 0.0f; - return; - } - - float drawScale = Math.max(0.0f, widgetScale); - float drawWidth = displayWidth * drawScale; - float drawHeight = displayHeight * drawScale; - if (drawWidth <= 0.0f || drawHeight <= 0.0f) { - width = 0.0f; - height = 0.0f; - return; - } - - float drawX = x + ((displayWidth - drawWidth) * 0.5f); - float drawY = y + ((displayHeight - drawHeight) * 0.5f); - float drawBaseScale = baseScale * drawScale; - float drawFontScale = fontScale * drawScale; - float drawHeaderIconH = headerIconH * drawScale; - float drawHeaderTextH = headerTextH * drawScale; - float drawRowTextH = rowTextH * drawScale; - int resolvedHeaderIconColor = uiCounter; - if (headerIconPulse.get()) { - int pulsedHeaderIconColor = HudTextEffects.animatedColor( - uiCounter, - HudTextEffects.Effect.PULSE, - headerIconPulseSpeed.get(), - (float) (Util.getMillis() / 1000.0), - 0.0f - ); - resolvedHeaderIconColor = HudRenderUtil.mixColor( - uiCounter, - pulsedHeaderIconColor, - headerIconPulseIntensity.get() / 100.0f - ); - } - - List> panelRows = ScriptedListHudPanel.rows(); - rowTextRenderer.begin(drawFontScale, false, false); - if (showExampleRow) { - ItemStack stack = previewStacks()[(int) ((System.currentTimeMillis() / 1000L) % previewStacks().length)]; - panelRows.add(ScriptedListHudPanel.row( - "preview", - "item", - BuiltInRegistries.ITEM.getKey(stack.getItem()).toString(), - List.of(ScriptedListHudPanel.textPart("Example Cooldowns", withAlpha(uiText & 0x00FFFFFF, 255), 0.0f)), - "**:**", - withAlpha(uiCounter & 0x00FFFFFF, 255), - withAlpha(uiMuted & 0x00FFFFFF, 130), - 1.0f - )); - } else { - for (AnimatedRow animatedRow : animatedRows) { - float anim = animatedRow.anim(); - if (anim <= 0.01f) continue; - Row rowData = animatedRow.row(); - int rowBaseAlpha = clamp255(Math.round(255.0f * anim)); - int iconAlpha = getBlinkAlpha(rowData, rowBaseAlpha, 110); - int textAlpha = getBlinkAlpha(rowData, rowBaseAlpha, 95); - int timeAlpha = getBlinkAlpha(rowData, rowBaseAlpha, 60); - panelRows.add(ScriptedListHudPanel.row( - rowData.key(), - "item", - BuiltInRegistries.ITEM.getKey(rowData.stack().getItem()).toString(), - List.of(ScriptedListHudPanel.textPart(rowData.name(), withAlpha(uiText & 0x00FFFFFF, textAlpha), 0.0f)), - rowData.duration(), - withAlpha(rowData.timeColor() & 0x00FFFFFF, timeAlpha), - withAlpha(uiMuted & 0x00FFFFFF, Math.max(26, iconAlpha - 120)), - anim - )); - } - } - rowTextRenderer.end(); - - scriptedPanel.render( - renderer, - textRenderer, - ctx, - tickDelta, - new ScriptedListHudPanel.Panel( - ScriptedListHudPanel.COOLDOWNS, - new ScriptedListHudPanel.Palette(uiHeaderLeft, uiHeaderRight, uiBodyLeft, uiBodyRight, - uiOutline, uiText, uiMuted, uiCounter, uiTitleText, uiDivider, uiBlurTint), - drawX, - drawY, - drawWidth, - drawHeight, - drawScale, - drawBaseScale, - drawFontScale, - drawHeaderIconH, - drawHeaderTextH, - drawRowTextH, - activeLabelWidth * drawScale, - activeCountWidth * drawScale, - rows.size(), - hasEffect(), - Math.min(1.0f, (blurAlpha.get() / 255.0f) * (isThemeMode() ? 1.15f : 1.0f)), - resolvedHeaderIconColor, - HudPanelLayoutModes.current(layoutMode), - panelRows - ) - ); - foregroundReady = false; - } - - @Override - public void renderEngineForeground(Renderer2D renderer, - TextRenderer textRenderer, - GuiGraphicsExtractor ctx, - float tickDelta, - int screenW, - int screenH) { - if (!foregroundReady || ctx == null || pendingItemTasks.isEmpty()) { - pendingItemTasks.clear(); - return; - } - for (QueuedItemIcon task : pendingItemTasks) { - // Cooldowns icons live in the same logical HUD space as the panel geometry/text. - renderer.item(task.stack(), task.x(), task.y(), task.scale(), task.seed(), Renderer2D.ITEM_OVERLAY_NONE, null); - } - pendingItemTasks.clear(); - } - - @Override - public boolean isDraggable() { - return super.isDraggable(); - } - - private void renderExampleRow(Renderer2D renderer, - TextRenderer rowTextRenderer, - float fontScale, - float rowTextH, - float baseScale, - float renderWidth, - float centerY, - float renderX) { - ItemStack stack = previewStacks()[(int) ((System.currentTimeMillis() / 1000L) % previewStacks().length)]; - int textColor = withAlpha(uiText & 0x00FFFFFF, 255); - int timeColor = withAlpha(uiCounter & 0x00FFFFFF, 255); - int dividerColor = withAlpha(uiMuted & 0x00FFFFFF, 130); - float rowContentCenterY = centerY + (ROW_CONTENT_CENTER_OFFSET * baseScale); - - queueItemIcon(pendingItemTasks, stack, renderX + (ROW_ICON_X * baseScale), rowContentCenterY, baseScale, 0); - - renderer.quad( - renderX + (ROW_DIVIDER_X * baseScale), - rowContentCenterY - ((ROW_DIVIDER_H * baseScale) * 0.5f), - Math.max(0.5f, 0.5f * baseScale), - ROW_DIVIDER_H * baseScale, - dividerColor - ); - - rowTextRenderer.begin(fontScale, false, false); - float textY = rowContentCenterY - (rowTextH * 0.5f); - rowTextRenderer.render("Example Cooldowns", - renderX + (ROW_TEXT_X * baseScale), textY, new RenderColor(textColor), false); - float durationWidth = (float) rowTextRenderer.getWidth("**:**", false); - rowTextRenderer.render("**:**", - renderX + renderWidth - durationWidth - (ROW_TIME_RIGHT * baseScale), - textY, - new RenderColor(timeColor), - false - ); - rowTextRenderer.end(); - } - - private void queueItemIcon(List itemTasks, - ItemStack stack, - float iconX, - float centerY, - float baseScale, - int seed) { - if (itemTasks == null) { - return; - } - if (stack.isEmpty()) { - return; - } - float iconSize = ROW_ICON_VISUAL_SIZE * baseScale; - float itemScale = iconSize / 16.0f; - float itemDrawX = iconX; - float itemDrawY = centerY - (16.0f * itemScale * 0.5f); - itemTasks.add(new QueuedItemIcon(stack.copy(), itemDrawX, itemDrawY, itemScale, seed)); - } - - private List collectRows() { - Minecraft mc = Minecraft.getInstance(); - List rows = new ArrayList<>(); - if (mc != null && mc.player != null) { - var manager = mc.player.getCooldowns(); - int nowTick = ((ItemCooldownManagerAccessor) manager).getTick(); - - ((ItemCooldownManagerAccessor) manager).getEntries().forEach((Identifier id, Object entry) -> { - int remainingTicks = ((ItemCooldownEntryAccessor) entry).getEndTick() - nowTick; - if (remainingTicks <= 0) return; - Item item = id != null ? BuiltInRegistries.ITEM.getValue(id) : Items.AIR; - ItemStack stack = new ItemStack(item); - String name = stack.isEmpty() ? (id != null ? id.getPath() : "Unknown") : stack.getHoverName().getString(); - String key = "vanilla:" + (id != null ? id.toString() : name); - rows.add(new Row(key, stack, name, formatSeconds(remainingTicks / 20.0f), remainingTicks, - uiMuted != 0 ? uiMuted : theme().textMuted())); - }); - - PvpCooldowns pvpCooldowns = Modules.get(PvpCooldowns.class); - boolean pvpEnabled = pvpCooldowns != null && pvpCooldowns.isSystemEnabled(); - if (pvpEnabled && pvpCooldowns.shouldRenderWidget()) { - boolean inPvp = CooldownsState.MANAGER.isInPvp(); - int stateColor = inPvp ? PVP_ACTIVE_COLOR : PVP_IDLE_COLOR; - for (Item item : CooldownRegistry.trackedItems()) { - ItemCooldownSnapshot snapshot = CooldownsState.MANAGER.snapshot(item); - if (!snapshot.visible()) continue; - - float seconds = snapshot.cooling() - ? snapshot.cooldownRemainingMs() / 1000.0f - : snapshot.useWindowRemainingMs() / 1000.0f; - String text = snapshot.cooling() - ? formatSeconds(seconds) - : snapshot.compactText(); - int remainingTicks = snapshot.cooling() - ? Math.max(1, Math.round(seconds * 20.0f)) - : Math.max(1, Math.round(Math.max(1.0f, seconds) * 20.0f)); - ItemStack stack = new ItemStack(item); - rows.add(new Row( - "pvp:" + BuiltInRegistries.ITEM.getKey(item), - stack, - stack.getHoverName().getString(), - text, - remainingTicks, - snapshot.cooling() ? stateColor : (uiMuted != 0 ? uiMuted : theme().textMuted()) - )); - } - } - - if (pvpEnabled && PvpState.isActive()) { - Float secondsLeft = PvpState.getSecondsLeft(); - if (secondsLeft != null && secondsLeft > 0.0f) { - int remainingTicks = Math.max(1, Math.round(secondsLeft * 20.0f)); - ItemStack pearlStack = new ItemStack(Items.ENDER_PEARL); - rows.add(new Row( - "pvpban:" + BuiltInRegistries.ITEM.getKey(Items.ENDER_PEARL), - pearlStack, - pearlStack.getHoverName().getString(), - formatSeconds(secondsLeft), - remainingTicks, - PVP_BLOCKED_COLOR - )); - ItemStack chorusStack = new ItemStack(Items.CHORUS_FRUIT); - rows.add(new Row( - "pvpban:" + BuiltInRegistries.ITEM.getKey(Items.CHORUS_FRUIT), - chorusStack, - chorusStack.getHoverName().getString(), - formatSeconds(secondsLeft), - remainingTicks, - PVP_BLOCKED_COLOR - )); - } - } - } - - rows.sort((a, b) -> String.CASE_INSENSITIVE_ORDER.compare(a.name(), b.name())); - return rows; - } - - private List animateRows(List rows) { - Map current = new LinkedHashMap<>(); - for (Row row : rows) { - current.put(row.key(), row); - } - - Map merged = new LinkedHashMap<>(); - merged.putAll(current); - for (Map.Entry entry : lastEntries.entrySet()) { - merged.putIfAbsent(entry.getKey(), entry.getValue()); - } - lastEntries.clear(); - lastEntries.putAll(merged); - - List toRemove = new ArrayList<>(); - for (Map.Entry entry : lastEntries.entrySet()) { - String key = entry.getKey(); - float target = current.containsKey(key) ? 1.0f : 0.0f; - float anim = entryAnim.getOrDefault(key, 0.0f); - anim = AnimationUtility.approach(anim, target, AnimationUtility.deltaTime(), 12.0f); - anim = AnimationUtility.snap(anim, target, 0.02f); - if (anim <= 0.0f && target <= 0.0f) { - toRemove.add(key); - } else { - entryAnim.put(key, anim); - } - } - for (String key : toRemove) { - entryAnim.remove(key); - lastEntries.remove(key); - } - - List out = new ArrayList<>(); - for (Map.Entry entry : lastEntries.entrySet()) { - float anim = entryAnim.getOrDefault(entry.getKey(), 0.0f); - if (anim > 0.01f) { - out.add(new AnimatedRow(entry.getValue(), anim)); - } - } - return out; - } - - private float totalAnimatedHeight(List rows, float rowStep) { - float out = 0.0f; - for (AnimatedRow row : rows) { - out += rowStep * row.anim(); - } - return out; - } - - private int getBlinkAlpha(Row row, int baseAlpha, int minAlphaFloor) { - if (!blink.get()) { - return baseAlpha; - } - if (row.remainingTicks() <= 0 || row.remainingTicks() > BLINK_THRESHOLD_TICKS) { - return baseAlpha; - } - - double wave = Math.sin(System.currentTimeMillis() * BLINK_SPEED); - float factor = (float) ((wave + 1.0) * 0.5); - int minAlpha = Math.max(0, Math.min(baseAlpha, minAlphaFloor)); - return clamp255(Math.round(minAlpha + (baseAlpha - minAlpha) * (1.0f - factor))); - } - - private void updatePalette() { - if (isThemeMode()) { - int alpha = bgAlpha.get(); - int window = HudRenderUtil.setAlpha(theme().windowBg(), alpha); - int header = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme().windowHeader(), theme().surface(), 0.18f), - Math.min(255, alpha + 14) - ); - int surface = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme().surface(), theme().windowBg(), 0.22f), - Math.min(255, alpha + 6) - ); - int deep = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme().surface(), theme().windowHeader(), 0.42f), - Math.min(255, alpha + 18) - ); - - uiHeaderLeft = HudRenderUtil.mixColor(header, window, 0.22f); - uiHeaderRight = HudRenderUtil.mixColor(surface, header, 0.52f); - uiBodyLeft = HudRenderUtil.mixColor(window, surface, 0.24f); - uiBodyRight = HudRenderUtil.mixColor(deep, surface, 0.18f); - uiOutline = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme().windowStroke(), theme().strokeSoft(), 0.18f), - Math.min(255, Math.max(182, alpha + 36)) - ); - uiText = theme().textPrimary(); - uiTitleText = HudRenderUtil.mixColor(theme().textPrimary(), theme().accent(), 0.14f); - uiMuted = HudRenderUtil.mixColor(theme().textMuted(), theme().textPrimary(), 0.16f); - uiCounter = HudRenderUtil.mixColor(theme().accent(), theme().textPrimary(), 0.38f); - uiDivider = HudRenderUtil.scaleAlpha(uiMuted, 0.68f); - uiBlurTint = HudRenderUtil.mixColor( - HudRenderUtil.mixColor(uiHeaderRight, uiBodyRight, 0.5f), - theme().accent(), - 0.14f - ); - return; - } - - uiHeaderLeft = bg.getArgb(); - uiHeaderRight = bg2.getArgb(); - uiBodyLeft = HudRenderUtil.mixColor(bg.getArgb(), bg2.getArgb(), 0.15f); - uiBodyRight = bg2.getArgb(); - uiOutline = stroke.getArgb(); - uiText = text.getArgb(); - uiTitleText = uiText; - uiMuted = muted.getArgb(); - uiCounter = HudRenderUtil.mixColor(text.getArgb(), 0xFFE1E1FF, 0.20f); - uiDivider = HudRenderUtil.scaleAlpha(uiMuted, 0.55f); - uiBlurTint = HudRenderUtil.mixColor(uiHeaderLeft, uiBodyRight, 0.5f); - } - - private String formatSeconds(float seconds) { - if (seconds >= 10.0f) { - return String.format("%.0fs", seconds); - } - return String.format("%.1fs", Math.round(seconds * 10.0f) / 10.0f); - } - - private int withAlpha(int rgb, int alpha) { - return (clamp255(alpha) << 24) | (rgb & 0x00FFFFFF); - } - - private boolean isThemeMode() { - return COLOR_THEME.equals(colorMode.get()); - } - - private boolean isCustomMode() { - return COLOR_CUSTOM.equals(colorMode.get()); - } - - private boolean hasEffect() { - return !EFFECT_NONE.equals(bgEffect.get()); - } - - private record Row(String key, ItemStack stack, String name, String duration, int remainingTicks, int timeColor) { - } - - private record AnimatedRow(Row row, float anim) { - } - - private record QueuedItemIcon(ItemStack stack, float x, float y, float scale, int seed) { - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Potions.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Potions.java deleted file mode 100644 index cf08144..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Potions.java +++ /dev/null @@ -1,623 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.features.gui.hud.draggable.impl.status; - -import dev.loki.lovisual.config.values.color.RGBAColorValue; -import dev.loki.lovisual.config.values.color.RGBColorValue; -import dev.loki.lovisual.config.values.mode.ModeValue; -import dev.loki.lovisual.config.values.primitive.BooleanValue; -import dev.loki.lovisual.config.values.primitive.NumberValue; -import dev.loki.lovisual.util.screen.ClientScreen; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.client.gui.screens.ChatScreen; -import net.minecraft.client.resources.language.I18n; -import net.minecraft.resources.Identifier; -import net.minecraft.resources.ResourceKey; -import net.minecraft.util.Util; -import net.minecraft.world.effect.MobEffectCategory; -import net.minecraft.world.effect.MobEffectInstance; -import dev.loki.lovisual.config.definition.SettingDef; -import dev.loki.lovisual.features.gui.hud.registry.HudElementRegister; -import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; -import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; -import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; -import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; -import dev.loki.lovisual.render.engine.animation.AnimationUtility; -import dev.loki.lovisual.render.engine.color.RenderColor; -import dev.loki.lovisual.render.engine.math.HudScale; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.text.font.info.FontInfo; -import dev.loki.lovisual.render.engine.text.font.Fonts; -import dev.loki.lovisual.render.engine.text.render.TextRenderer; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import static dev.loki.lovisual.features.theme.Theme.theme; -import dev.loki.lovisual.features.gui.hud.draggable.impl.HudPanelLayoutModes; -import dev.loki.lovisual.features.gui.hud.draggable.impl.scripted.ScriptedListHudPanel; - -@HudElementRegister(order = 20) -public final class Potions extends DraggableHudElement { - - { - defaultLayout(31.729004f, 182.0f); - } - - private static final float HEADER_HEIGHT = 15.5f; - private static final float BODY_Y_OFFSET = 18.5f; - private static final float CONTENT_START_Y = 25.0f; - private static final float BODY_INSET_Y = 6.5f; - private static final float ROW_STEP = 11.0f; - private static final float MIN_WIDTH = 95.0f; - private static final float PANEL_RADIUS = 4.0f; - private static final float PANEL_SOFTNESS = 1.0f; - private static final float PANEL_STROKE = 0.55f; - private static final float HEADER_DIVIDER_X = 18.0f; - private static final float HEADER_DIVIDER_Y = 5.0f; - private static final float HEADER_DIVIDER_H = 6.0f; - private static final float ROW_DIVIDER_X = 14.0f; - private static final float ROW_DIVIDER_H = 7.0f; - private static final float TITLE_ICON_X = 5.0f; - private static final float TITLE_TEXT_X = 22.0f; - private static final float COUNT_LABEL_OFFSET = 22.0f; - private static final float COUNT_VALUE_OFFSET = 3.0f; - private static final float ROW_ICON_X = 3.5f; - private static final float ROW_ICON_SIZE = 8.0f; - private static final float ROW_TEXT_X = 18.0f; - private static final float ROW_TIME_RIGHT = 8.0f; - private static final float ROW_CONTENT_CENTER_OFFSET = 2.1f; - private static final int BLINK_THRESHOLD_TICKS = 200; - private static final double BLINK_SPEED = 0.009; - private static final String COLOR_THEME = "Theme"; - private static final String COLOR_CUSTOM = "Custom"; - private static final String EFFECT_NONE = "None"; - private static final String EFFECT_BLUR = "Blur"; - private static final int NEGATIVE_EFFECT_RGB = 0xFFFF554B; - private static final Identifier[] PREVIEW_ICONS = new Identifier[]{ - Identifier.fromNamespaceAndPath("minecraft", "textures/mob_effect/speed.png"), - Identifier.fromNamespaceAndPath("minecraft", "textures/mob_effect/haste.png"), - Identifier.fromNamespaceAndPath("minecraft", "textures/mob_effect/fire_resistance.png"), - Identifier.fromNamespaceAndPath("minecraft", "textures/mob_effect/regeneration.png"), - Identifier.fromNamespaceAndPath("minecraft", "textures/mob_effect/night_vision.png") - }; - - private final HudGlobalConfig hud = HudGlobalConfig.get(); - private final ScriptedListHudPanel scriptedPanel = new ScriptedListHudPanel(); - private final NumberValue scaleValue = - new NumberValue<>("potions_scale", 1.05, HudPanelLayoutModes.SCALE_MIN, HudPanelLayoutModes.SCALE_MAX); - private final ModeValue layoutMode = - new ModeValue("potions_layout", "Unified Divider", HudPanelLayoutModes.SPLIT_HEADER, HudPanelLayoutModes.UNIFIED_DIVIDER); - private final ModeValue colorMode = - new ModeValue("potions_color_mode", "Theme", COLOR_THEME, COLOR_CUSTOM); - private final ModeValue bgEffect = - new ModeValue("potions_bg_effect", "Blur", EFFECT_NONE, EFFECT_BLUR); - private final RGBAColorValue bg = - new RGBAColorValue("potions_bg", "#EB784F4F"); - private final RGBAColorValue bg2 = - new RGBAColorValue("potions_bg_secondary", "#F7161616"); - private final NumberValue bgAlpha = - new NumberValue<>("potions_bg_alpha", 200, 0, 255); - private final RGBColorValue stroke = - new RGBColorValue("potions_stroke", "#5A5A5A"); - private final RGBColorValue text = - new RGBColorValue("potions_text", "#FFFFFF"); - private final RGBColorValue muted = - new RGBColorValue("potions_muted", "#A5A5A5"); - private final NumberValue blurAlpha = - new NumberValue<>("potions_blur_alpha", 159, 0, 255); - private final BooleanValue headerIconPulse = - new BooleanValue("potions_header_icon_pulse", true); - private final NumberValue headerIconPulseSpeed = - new NumberValue<>("potions_header_icon_pulse_speed", 18, 1, 60); - private final NumberValue headerIconPulseIntensity = - new NumberValue<>("potions_header_icon_pulse_intensity", 100, 0, 100); - - private final Map entryAnim = new LinkedHashMap<>(); - private final Map lastEntries = new LinkedHashMap<>(); - private float displayWidth = -1.0f; - private float displayHeight = -1.0f; - private float visibilityAnim = 0.0f; - private int uiHeaderLeft; - private int uiHeaderRight; - private int uiBodyLeft; - private int uiBodyRight; - private int uiOutline; - private int uiText; - private int uiMuted; - private int uiCounter; - private int uiTitleText; - private int uiAmpText; - private int uiDivider; - private int uiBlurTint; - - public Potions() { - super("potions", "Potions", true); - } - - private static int clamp255(int value) { - return Math.max(0, Math.min(255, value)); - } - - @Override - protected void defineSettings(List defs) { - defs.add(SettingDef.number(scaleValue)); - defs.add(SettingDef.mode(layoutMode)); - defs.add(SettingDef.mode(colorMode)); - defs.add(SettingDef.color(bg).visibleWhen(this::isCustomMode)); - defs.add(SettingDef.color(bg2).visibleWhen(this::isCustomMode)); - defs.add(SettingDef.colorNoAlpha(stroke).visibleWhen(this::isCustomMode)); - defs.add(SettingDef.number(bgAlpha).visibleWhen(this::isThemeMode)); - defs.add(SettingDef.colorNoAlpha(text).visibleWhen(this::isCustomMode)); - defs.add(SettingDef.colorNoAlpha(muted).visibleWhen(this::isCustomMode)); - defs.add(SettingDef.mode(bgEffect)); - defs.add(SettingDef.number(blurAlpha).visibleWhen(this::hasEffect)); - defs.add(SettingDef.bool(headerIconPulse)); - defs.add(SettingDef.number(headerIconPulseSpeed) - .visibleWhen(headerIconPulse::get)); - defs.add(SettingDef.number(headerIconPulseIntensity) - .visibleWhen(headerIconPulse::get)); - } - - @Override - public void applyDefaultPosition(int screenW, int screenH) { - this.x = screenW - 120.0f; - this.y = 20.0f; - } - - @Override - public boolean usesEngineRenderer() { - return true; - } - - @Override - public void renderEngine(Renderer2D renderer, - TextRenderer textRenderer, - GuiGraphicsExtractor ctx, - float tickDelta, - int screenW, - int screenH) { - boolean forceVisible = DraggableHudElementRegistry.isForceVisible(); - Minecraft mc = Minecraft.getInstance(); - if (mc == null || (mc.player == null && !forceVisible)) { - width = 0.0f; - height = 0.0f; - return; - } - - boolean chatPreview = ClientScreen.current() instanceof ChatScreen; - List rows = collectRows(); - List animatedRows = animateRows(rows); - boolean showExampleRow = rows.isEmpty() && (forceVisible || chatPreview); - boolean showWidget = !rows.isEmpty() || showExampleRow; - visibilityAnim = HudRenderUtil.animateVisibility(visibilityAnim, showWidget); - float widgetScale = HudRenderUtil.visibilityScale(visibilityAnim); - if (animatedRows.isEmpty() && !showExampleRow && visibilityAnim <= 0.0f) { - width = 0.0f; - height = 0.0f; - return; - } - - updatePalette(); - - TextRenderer headerIconRenderer = Fonts.renderer("IconsNur", FontInfo.Type.Regular, TextRenderer.get()); - TextRenderer headerTextRenderer = Fonts.renderer("Inter", FontInfo.Type.Bold, TextRenderer.get()); - TextRenderer rowTextRenderer = Fonts.renderer("OnestMedium", FontInfo.Type.Regular, TextRenderer.get()); - if (headerTextRenderer == null) headerTextRenderer = textRenderer; - if (rowTextRenderer == null) rowTextRenderer = textRenderer; - if (headerIconRenderer == null) headerIconRenderer = textRenderer; - - float baseScale = HudScale.scale(screenW, screenH) * 1.1f * HudPanelLayoutModes.effectiveScale(scaleValue); - float rowStep = ROW_STEP * baseScale; - float fontScale = 0.98f * (hud.getFontSize() / 18.0f); - - headerIconRenderer.begin(fontScale, true, false); - float headerIconH = (float) headerIconRenderer.getHeight(false); - headerIconRenderer.end(); - - headerTextRenderer.begin(fontScale, true, false); - float headerTextH = (float) headerTextRenderer.getHeight(false); - float titleWidth = (float) headerTextRenderer.getWidth("Potions", false); - headerTextRenderer.end(); - - rowTextRenderer.begin(fontScale, true, false); - float rowTextH = (float) rowTextRenderer.getHeight(false); - float maxWidth = MIN_WIDTH * baseScale; - if (showExampleRow) { - float previewWidth = (float) rowTextRenderer.getWidth("Example effect**:**", false) + (30.0f * baseScale); - maxWidth = Math.max(maxWidth, previewWidth); - } else { - for (AnimatedRow animatedRow : animatedRows) { - Row row = animatedRow.row(); - String label = row.name() + (row.amp().isEmpty() ? "" : " " + row.amp()); - float widthCandidate = (float) rowTextRenderer.getWidth(label + row.duration(), false) + (30.0f * baseScale); - maxWidth = Math.max(maxWidth, widthCandidate); - } - } - rowTextRenderer.end(); - - rowTextRenderer.begin(fontScale * 0.92f, false, false); - String activeCountText = Integer.toString(rows.size()); - float activeLabelWidth = (float) rowTextRenderer.getWidth("Active:", false); - float activeCountWidth = (float) rowTextRenderer.getWidth(activeCountText, false); - rowTextRenderer.end(); - float headerWidth = (TITLE_TEXT_X * baseScale) + titleWidth - + activeLabelWidth + activeCountWidth - + ((COUNT_LABEL_OFFSET + 12.0f) * baseScale); - maxWidth = Math.max(maxWidth, headerWidth); - - float contentHeight = showExampleRow ? rowStep : totalAnimatedHeight(animatedRows, rowStep); - float targetHeight = (showExampleRow || !animatedRows.isEmpty()) - ? (CONTENT_START_Y * baseScale + contentHeight) - : (HEADER_HEIGHT * baseScale); - - displayWidth = HudRenderUtil.animateDimension(displayWidth, maxWidth); - displayHeight = HudRenderUtil.animateDimension(displayHeight, targetHeight); - width = displayWidth; - height = displayHeight; - - if (!showWidget && widgetScale <= 0.001f && animatedRows.isEmpty()) { - width = 0.0f; - height = 0.0f; - return; - } - - float drawScale = Math.max(0.0f, widgetScale); - float drawWidth = displayWidth * drawScale; - float drawHeight = displayHeight * drawScale; - if (drawWidth <= 0.0f || drawHeight <= 0.0f) { - width = 0.0f; - height = 0.0f; - return; - } - - float drawX = x + ((displayWidth - drawWidth) * 0.5f); - float drawY = y + ((displayHeight - drawHeight) * 0.5f); - float drawBaseScale = baseScale * drawScale; - float drawFontScale = fontScale * drawScale; - float drawHeaderIconH = headerIconH * drawScale; - float drawHeaderTextH = headerTextH * drawScale; - float drawRowTextH = rowTextH * drawScale; - int resolvedHeaderIconColor = uiCounter; - if (headerIconPulse.get()) { - int pulsedHeaderIconColor = HudTextEffects.animatedColor( - uiCounter, - HudTextEffects.Effect.PULSE, - headerIconPulseSpeed.get(), - (float) (Util.getMillis() / 1000.0), - 0.0f - ); - resolvedHeaderIconColor = HudRenderUtil.mixColor( - uiCounter, - pulsedHeaderIconColor, - headerIconPulseIntensity.get() / 100.0f - ); - } - - List> panelRows = ScriptedListHudPanel.rows(); - rowTextRenderer.begin(drawFontScale, false, false); - if (showExampleRow) { - Identifier previewIcon = PREVIEW_ICONS[(int) ((System.currentTimeMillis() / 1000L) % PREVIEW_ICONS.length)]; - List> parts = List.of( - ScriptedListHudPanel.textPart("Example effect", withAlpha(uiText & 0x00FFFFFF, 255), 0.0f) - ); - LinkedHashMap row = ScriptedListHudPanel.row( - "preview", - "texture", - ScriptedListHudPanel.idString(previewIcon), - parts, - "**:**", - withAlpha(uiCounter & 0x00FFFFFF, 255), - withAlpha(uiMuted & 0x00FFFFFF, 130), - 1.0f - ); - row.put("iconTint", ScriptedListHudPanel.hex(withAlpha(0x00FFFFFF, 255))); - panelRows.add(row); - } else { - for (AnimatedRow animatedRow : animatedRows) { - float anim = animatedRow.anim(); - if (anim <= 0.01f) continue; - Row rowData = animatedRow.row(); - int rowBaseAlpha = clamp255(Math.round(255.0f * anim)); - int blinkAlpha = getBlinkAlpha(rowData, rowBaseAlpha, 100); - boolean bad = isBadEffect(rowData.effect()); - int nameColor = withAlpha(bad ? NEGATIVE_EFFECT_RGB : (uiText & 0x00FFFFFF), blinkAlpha); - int dividerColor = withAlpha((bad ? NEGATIVE_EFFECT_RGB : (uiMuted & 0x00FFFFFF)), Math.max(24, blinkAlpha - 125)); - List> parts = new ArrayList<>(2); - parts.add(ScriptedListHudPanel.textPart(rowData.name(), nameColor, 0.0f)); - if (!rowData.amp().isEmpty()) { - float labelWidth = (float) rowTextRenderer.getWidth(rowData.name(), false); - parts.add(ScriptedListHudPanel.textPart(" " + rowData.amp(), withAlpha(uiAmpText & 0x00FFFFFF, blinkAlpha), labelWidth)); - } - LinkedHashMap row = ScriptedListHudPanel.row( - rowData.key(), - "texture", - ScriptedListHudPanel.idString(rowData.iconId()), - parts, - rowData.duration(), - withAlpha(uiCounter & 0x00FFFFFF, blinkAlpha), - dividerColor, - anim - ); - row.put("iconTint", ScriptedListHudPanel.hex(withAlpha(0x00FFFFFF, blinkAlpha))); - panelRows.add(row); - } - } - rowTextRenderer.end(); - - scriptedPanel.render( - renderer, - textRenderer, - ctx, - tickDelta, - new ScriptedListHudPanel.Panel( - ScriptedListHudPanel.POTIONS, - new ScriptedListHudPanel.Palette(uiHeaderLeft, uiHeaderRight, uiBodyLeft, uiBodyRight, - uiOutline, uiText, uiMuted, uiCounter, uiTitleText, uiDivider, uiBlurTint), - drawX, - drawY, - drawWidth, - drawHeight, - drawScale, - drawBaseScale, - drawFontScale, - drawHeaderIconH, - drawHeaderTextH, - drawRowTextH, - activeLabelWidth * drawScale, - activeCountWidth * drawScale, - rows.size(), - hasEffect(), - Math.min(1.0f, (blurAlpha.get() / 255.0f) * (isThemeMode() ? 1.15f : 1.0f)), - resolvedHeaderIconColor, - HudPanelLayoutModes.current(layoutMode), - panelRows - ) - ); - } - - @Override - public boolean isDraggable() { - return super.isDraggable(); - } - - private void renderExampleRow(Renderer2D renderer, - TextRenderer rowTextRenderer, - float fontScale, - float rowTextH, - float baseScale, - float renderWidth, - float centerY, - float renderX) { - int textColor = withAlpha(uiText & 0x00FFFFFF, 255); - int timeColor = withAlpha(uiCounter & 0x00FFFFFF, 255); - int dividerColor = withAlpha(uiMuted & 0x00FFFFFF, 130); - float iconSize = ROW_ICON_SIZE * baseScale; - float rowContentCenterY = centerY + (ROW_CONTENT_CENTER_OFFSET * baseScale); - Identifier previewIcon = PREVIEW_ICONS[(int) ((System.currentTimeMillis() / 1000L) % PREVIEW_ICONS.length)]; - - Renderer2D.TEXTURE.roundedTexRect( - renderX + (ROW_ICON_X * baseScale), - rowContentCenterY - (iconSize * 0.5f), - iconSize, - iconSize, - Math.min(1.15f * baseScale, iconSize * 0.18f), - 1.0f, - withAlpha(0x00FFFFFF, 255), - previewIcon - ); - - renderer.quad( - renderX + (ROW_DIVIDER_X * baseScale), - rowContentCenterY - ((ROW_DIVIDER_H * baseScale) * 0.5f), - Math.max(0.5f, 0.5f * baseScale), - ROW_DIVIDER_H * baseScale, - dividerColor - ); - - rowTextRenderer.begin(fontScale, false, false); - float textY = rowContentCenterY - (rowTextH * 0.5f); - rowTextRenderer.render("Example effect", - renderX + (ROW_TEXT_X * baseScale), textY, new RenderColor(textColor), false); - float durationWidth = (float) rowTextRenderer.getWidth("**:**", false); - rowTextRenderer.render("**:**", - renderX + renderWidth - durationWidth - (ROW_TIME_RIGHT * baseScale), - textY, - new RenderColor(timeColor), - false - ); - rowTextRenderer.end(); - } - - private List collectRows() { - List rows = new ArrayList<>(); - Minecraft mc = Minecraft.getInstance(); - if (mc != null && mc.player != null) { - mc.player.getActiveEffects().stream() - .sorted((a, b) -> String.CASE_INSENSITIVE_ORDER.compare( - I18n.get(a.getDescriptionId()), - I18n.get(b.getDescriptionId()) - )) - .forEach(inst -> { - String name = I18n.get(inst.getDescriptionId()); - String amp = inst.getAmplifier() > 0 ? Integer.toString(inst.getAmplifier() + 1) : ""; - String duration = formatDuration(inst); - Identifier effectId = inst.getEffect().unwrapKey().map(ResourceKey::identifier).orElse(null); - String key = effectId != null ? effectId.toString() : name; - Identifier iconId = effectId != null - ? Identifier.fromNamespaceAndPath("minecraft", "textures/mob_effect/" + effectId.getPath() + ".png") - : null; - rows.add(new Row(key, inst, name, amp, duration, iconId)); - }); - } - return rows; - } - - private List animateRows(List rows) { - Map current = new LinkedHashMap<>(); - for (Row row : rows) { - current.put(row.key(), row); - } - - Map merged = new LinkedHashMap<>(); - merged.putAll(current); - for (Map.Entry entry : lastEntries.entrySet()) { - merged.putIfAbsent(entry.getKey(), entry.getValue()); - } - lastEntries.clear(); - lastEntries.putAll(merged); - - List toRemove = new ArrayList<>(); - for (Map.Entry entry : lastEntries.entrySet()) { - String key = entry.getKey(); - float target = current.containsKey(key) ? 1.0f : 0.0f; - float anim = entryAnim.getOrDefault(key, 0.0f); - anim = AnimationUtility.approach(anim, target, AnimationUtility.deltaTime(), 12.0f); - anim = AnimationUtility.snap(anim, target, 0.02f); - if (anim <= 0.0f && target <= 0.0f) { - toRemove.add(key); - } else { - entryAnim.put(key, anim); - } - } - for (String key : toRemove) { - entryAnim.remove(key); - lastEntries.remove(key); - } - - List out = new ArrayList<>(); - for (Map.Entry entry : lastEntries.entrySet()) { - float anim = entryAnim.getOrDefault(entry.getKey(), 0.0f); - if (anim > 0.01f) { - out.add(new AnimatedRow(entry.getValue(), anim)); - } - } - return out; - } - - private float totalAnimatedHeight(List rows, float rowStep) { - float out = 0.0f; - for (AnimatedRow row : rows) { - out += rowStep * row.anim(); - } - return out; - } - - private String formatDuration(MobEffectInstance inst) { - int rawTicks = inst.getDuration(); - if (rawTicks < 0) { - return "**:**"; - } - int ticks = Math.max(0, rawTicks); - int minutes = ticks / 1200; - if (minutes > 60) { - return "**:**"; - } - return minutes + ":" + String.format("%02d", (ticks % 1200) / 20); - } - - private int getBlinkAlpha(Row row, int baseAlpha, int minAlphaFloor) { - MobEffectInstance inst = row.effect(); - if (inst == null) { - return baseAlpha; - } - int duration = inst.getDuration(); - if (duration <= 0 || duration > BLINK_THRESHOLD_TICKS) { - return baseAlpha; - } - - double wave = Math.sin(System.currentTimeMillis() * BLINK_SPEED); - float factor = (float) ((wave + 1.0) * 0.5); - int minAlpha = Math.max(0, Math.min(baseAlpha, minAlphaFloor)); - return clamp255(Math.round(minAlpha + (baseAlpha - minAlpha) * (1.0f - factor))); - } - - private boolean isBadEffect(MobEffectInstance effect) { - return effect != null && effect.getEffect().value().getCategory() == MobEffectCategory.HARMFUL; - } - - private void updatePalette() { - if (isThemeMode()) { - int alpha = bgAlpha.get(); - int window = HudRenderUtil.setAlpha(theme().windowBg(), alpha); - int header = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme().windowHeader(), theme().surface(), 0.18f), - Math.min(255, alpha + 14) - ); - int surface = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme().surface(), theme().windowBg(), 0.22f), - Math.min(255, alpha + 6) - ); - int deep = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme().surface(), theme().windowHeader(), 0.42f), - Math.min(255, alpha + 18) - ); - - uiHeaderLeft = HudRenderUtil.mixColor(header, window, 0.22f); - uiHeaderRight = HudRenderUtil.mixColor(surface, header, 0.52f); - uiBodyLeft = HudRenderUtil.mixColor(window, surface, 0.24f); - uiBodyRight = HudRenderUtil.mixColor(deep, surface, 0.18f); - uiOutline = HudRenderUtil.setAlpha( - HudRenderUtil.mixColor(theme().windowStroke(), theme().strokeSoft(), 0.18f), - Math.min(255, Math.max(182, alpha + 36)) - ); - uiText = theme().textPrimary(); - uiTitleText = HudRenderUtil.mixColor(theme().textPrimary(), theme().accent(), 0.14f); - uiMuted = HudRenderUtil.mixColor(theme().textMuted(), theme().textPrimary(), 0.16f); - uiCounter = HudRenderUtil.mixColor(theme().accent(), theme().textPrimary(), 0.38f); - uiAmpText = HudRenderUtil.mixColor(theme().textPrimary(), theme().accent(), 0.22f); - uiDivider = HudRenderUtil.scaleAlpha(uiMuted, 0.68f); - uiBlurTint = HudRenderUtil.mixColor( - HudRenderUtil.mixColor(uiHeaderRight, uiBodyRight, 0.5f), - theme().accent(), - 0.14f - ); - return; - } - - uiHeaderLeft = bg.getArgb(); - uiHeaderRight = bg2.getArgb(); - uiBodyLeft = HudRenderUtil.mixColor(bg.getArgb(), bg2.getArgb(), 0.15f); - uiBodyRight = bg2.getArgb(); - uiOutline = stroke.getArgb(); - uiText = text.getArgb(); - uiTitleText = uiText; - uiMuted = muted.getArgb(); - uiCounter = HudRenderUtil.mixColor(text.getArgb(), 0xFFE1E1FF, 0.20f); - uiAmpText = uiText; - uiDivider = HudRenderUtil.scaleAlpha(uiMuted, 0.55f); - uiBlurTint = HudRenderUtil.mixColor(uiHeaderLeft, uiBodyRight, 0.5f); - } - - private int withAlpha(int rgb, int alpha) { - return (clamp255(alpha) << 24) | (rgb & 0x00FFFFFF); - } - - private boolean isThemeMode() { - return COLOR_THEME.equals(colorMode.get()); - } - - private boolean isCustomMode() { - return COLOR_CUSTOM.equals(colorMode.get()); - } - - private boolean hasEffect() { - return !EFFECT_NONE.equals(bgEffect.get()); - } - - private record Row(String key, MobEffectInstance effect, String name, String amp, String duration, - Identifier iconId) { - } - - private record AnimatedRow(Row row, float anim) { - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/CustomTabList.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/CustomTabList.java deleted file mode 100644 index 894a906..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/CustomTabList.java +++ /dev/null @@ -1,834 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.features.gui.hud.nondraggable.impl.tab; - -import dev.loki.lovisual.config.values.primitive.BooleanValue; -import dev.loki.lovisual.config.values.primitive.NumberValue; -import dev.loki.lovisual.config.values.color.RGBAColorValue; -import dev.loki.lovisual.config.values.color.RGBColorValue; -import dev.loki.lovisual.features.gui.hud.base.AbstractHudElement; -import dev.loki.lovisual.features.gui.hud.registry.HudElementRegister; -import dev.loki.lovisual.features.gui.hud.anchor.HudRenderSpace; -import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; -import dev.loki.lovisual.features.module.phase.HudPhase; -import dev.loki.lovisual.features.relations.category.CategoryType; -import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; -import dev.loki.lovisual.render.engine.core.frame.RenderPhase; -import dev.loki.lovisual.render.engine.core.frame.RenderPhaseScope; -import dev.loki.lovisual.render.engine.core.context.ViewportContext; -import dev.loki.lovisual.render.engine.math.HudScale; -import dev.loki.lovisual.render.helpers.util.TickDelta; -import dev.loki.lovisual.render.engine.animation.AnimationUtility; -import dev.loki.lovisual.render.engine.color.RenderColor; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.helpers.player.PlayerHeadRenderer; -import dev.loki.lovisual.Lifecycle; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.network.chat.Component; -import net.minecraft.world.scores.DisplaySlot; -import net.minecraft.world.scores.Objective; -import net.minecraft.world.scores.Scoreboard; -import net.minecraft.util.Util; - -import java.util.List; - -import static dev.loki.lovisual.features.theme.Theme.theme; -import dev.loki.lovisual.render.engine.renderer.ui.batches.ordered.OrderedUiBatcher; -import dev.loki.lovisual.features.gui.hud.nondraggable.impl.tab.TabListAnimator; -import dev.loki.lovisual.features.gui.hud.nondraggable.impl.tab.TabListModel; -import dev.loki.lovisual.features.gui.hud.nondraggable.impl.tab.TabRichTextRenderer; - -@HudElementRegister(order = 95) -public final class CustomTabList extends AbstractHudElement { - public static final CustomTabList INSTANCE = new CustomTabList(); - - private static final float TOP_Y = 56f; - private static final float HEAD_SIZE = 30f; - private static final float ROW_PAD_X = 10.5f; - private static final float TEXT_SIZE = 24.5f; - private static final float PING_TEXT_SIZE = 17.5f; - private static final float SCORE_TEXT_SIZE = 20.5f; - private static final float HEADER_TEXT_SIZE = 24f; - private static final float FOOTER_TEXT_SIZE = 20.5f; - private static final float SHELL_PAD_X = 14f; - - private static final long STRUCTURE_PROBE_INTERVAL_MS = 250L; - private static final long DYNAMIC_REFRESH_INTERVAL_MS = 500L; - - private final TabListAnimator animator = new TabListAnimator(); - private final NumberValue maxWidth = num("tab_list_max_width", "max_width", 1680, 760, 2600); - private final BooleanValue syncTheme = bool("tab_list_sync_theme", "sync_theme", true); - private final NumberValue themeAlpha = visibleWhen(num("tab_list_theme_alpha", "theme_alpha", 225, 0, 255), syncTheme::get); - private final RGBAColorValue bgColor = visibleWhen(color("tab_list_bg", "background", "#E50B1018"), () -> !syncTheme.get()); - private final RGBColorValue accentColor = visibleWhen(colorNoAlpha("tab_list_accent", "accent", "#6E8DFF"), () -> !syncTheme.get()); - private final RGBColorValue textColor = visibleWhen(colorNoAlpha("tab_list_text", "text", "#F4F7FB"), () -> !syncTheme.get()); - private final RGBColorValue mutedColor = visibleWhen(colorNoAlpha("tab_list_muted", "muted", "#9AA5B5"), () -> !syncTheme.get()); - - private TabListModel.Snapshot snapshot = TabListModel.Snapshot.empty(); - private long lastStructureProbeMs; - private long lastDynamicRefreshMs; - private long lastStructureSignature = Long.MIN_VALUE; - private int lastSnapshotWidth = -1; - private boolean wasShown; - private int uiFillTop; - private int uiFillBottom; - private int uiFillHotspot; - private int uiStroke; - private int uiStrokeSoft; - private int uiAccent; - private int uiAccentSoft; - private int uiAccentGlow; - private int uiText; - private int uiMuted; - private int uiRowFill; - private int uiRowAltFill; - private int uiRowHighlight; - private int uiPingChip; - private int uiDivider; - - private CustomTabList() { - super("tab_list", "Tab List", true); - } - - public static boolean shouldReplaceVanilla() { - return Lifecycle.canRunHud() - && INSTANCE != null - && INSTANCE.isEnabled() - && INSTANCE.canRenderReplacement(); - } - - public static void renderVanillaTabStratum(GuiGraphicsExtractor ctx, int guiWidth) { - CustomTabList instance = INSTANCE; - if (instance == null) return; - instance.renderVanillaTabStratumInternal(ctx, guiWidth); - } - - private void renderVanillaTabStratumInternal(GuiGraphicsExtractor ctx, int guiWidth) { - Minecraft mc = Minecraft.getInstance(); - if (!Lifecycle.canRunHud()) return; - if (mc == null || mc.getWindow() == null || ctx == null) return; - int screenW = Math.max(1, Math.round(HudScale.virtualWidth( - mc.getWindow().getWidth(), - mc.getWindow().getHeight() - ))); - int screenH = Math.max(1, Math.round(HudScale.virtualHeight( - mc.getWindow().getWidth(), - mc.getWindow().getHeight() - ))); - - LoVisualRenderSystem.ensureFrameContext(); - LoVisualRenderSystem.updateFrameTiming( - TickDelta.tickProgress(false), - TickDelta.frameDeltaTicks(), - TickDelta.fixedDeltaTicks() - ); - ViewportContext.beginCurrentStratumUnscaledLogical(ctx); - try (RenderPhaseScope ignored = LoVisualRenderSystem.phase(RenderPhase.SCREEN_TOP, "2d:vanilla_stratum:tab_list")) { - Renderer2D.COLOR.begin(); - renderTab(Renderer2D.COLOR, ctx, TickDelta.tickProgress(false), screenW, screenH, true); - Renderer2D.COLOR.render(); - } finally { - ViewportContext.endCurrentStratum(ctx); - } - } - - @Override - public boolean usesEngineRenderer() { - return true; - } - - @Override - public HudRenderSpace getRenderSpace() { - return HudRenderSpace.UNSCALED_LOGICAL; - } - - @Override - public HudPhase getHudPhase() { - return HudPhase.NONE; - } - - @Override - public int getRenderOrder() { - return 1000; - } - - @Override - public void renderEngineForeground(Renderer2D renderer, - dev.loki.lovisual.render.engine.text.render.TextRenderer textRenderer, - GuiGraphicsExtractor ctx, - float tickDelta, - int screenW, - int screenH) { - renderTab(renderer, ctx, tickDelta, screenW, screenH, isEnabled() && shouldShowNow()); - } - - private void renderTab(Renderer2D renderer, - GuiGraphicsExtractor ctx, - float tickDelta, - int screenW, - int screenH, - boolean show) { - Minecraft mc = Minecraft.getInstance(); - if (mc == null || ctx == null || renderer == null) return; - float panelPresence = animator.updatePanel(show); - if (!show && panelPresence <= 0.01f && !animator.hasVisibleRows()) { - setBounds(0f, 0f, 0f, 0f); - return; - } - - if (show) { - refreshSnapshot(screenW, !wasShown); - } - wasShown = show; - animator.updateRows(show ? snapshot.visibleIds() : null); - - float panelAlpha = AnimationUtility.easeOutCubic(panelPresence); - float width = tabPanelWidth(screenW); - float height = snapshot.height(); - float drawX = (screenW - width) * 0.5f; - float drawY = TOP_Y - (1f - panelAlpha) * 18f; - setBounds(drawX, drawY, width, height); - - updatePalette(); - renderNativeChrome(renderer, drawX, drawY, width, height, panelAlpha); - renderHeadsAndText(ctx, renderer, drawX, drawY, width, panelAlpha); - } - - private void refreshSnapshot(int screenW, boolean force) { - Minecraft mc = Minecraft.getInstance(); - if (mc == null || mc.getConnection() == null) return; - long now = Util.getMillis(); - int listedPlayers = Math.min(mc.getConnection().getListedOnlinePlayers().size(), TabListModel.maxPlayers()); - boolean multiWide = listedPlayers > TabListModel.maxRowsPerColumn() * 2; - int effectiveWidth = Math.max(1, Math.min(screenW, multiWide ? screenW : maxWidth.get())); - boolean needCollect = force - || snapshot.entries().isEmpty() - || snapshot.entries().size() != listedPlayers - || effectiveWidth != lastSnapshotWidth; - long signature = lastStructureSignature; - if (needCollect || now - lastStructureProbeMs >= STRUCTURE_PROBE_INTERVAL_MS) { - signature = TabListModel.structureSignature(mc, effectiveWidth); - lastStructureProbeMs = now; - if (signature != lastStructureSignature) { - needCollect = true; - } - } - - if (needCollect) { - snapshot = TabListModel.collect(mc, effectiveWidth); - lastSnapshotWidth = effectiveWidth; - lastStructureSignature = signature; - lastDynamicRefreshMs = now; - return; - } - - if (now - lastDynamicRefreshMs >= DYNAMIC_REFRESH_INTERVAL_MS) { - snapshot = TabListModel.refreshDynamic(mc, snapshot); - lastDynamicRefreshMs = now; - } - } - - private float tabPanelWidth(int screenW) { - float screenCap = Math.max(360f, screenW - 24f); - float configured = Math.max(1f, maxWidth.get()); - float wanted = snapshot.width(); - if (snapshot.columns() > 2) { - configured = Math.max(configured, wanted); - } - return Math.min(screenCap, Math.min(configured, wanted)); - } - - private boolean canRenderReplacement() { - Minecraft mc = Minecraft.getInstance(); - return mc != null && mc.player != null && mc.getConnection() != null; - } - - private boolean shouldShowNow() { - Minecraft mc = Minecraft.getInstance(); - if (mc == null || mc.player == null || mc.getConnection() == null || mc.options == null) return false; - if (!mc.options.keyPlayerList.isDown()) return false; - - int listedPlayers = mc.getConnection().getListedOnlinePlayers().size(); - if (listedPlayers <= 0) return false; - if (!mc.isLocalServer() || listedPlayers > 1) return true; - - Objective listObjective = null; - if (mc.level != null) { - Scoreboard scoreboard = mc.level.getScoreboard(); - if (scoreboard != null) { - listObjective = scoreboard.getDisplayObjective(DisplaySlot.LIST); - } - } - return listObjective != null; - } - - private void renderNativeChrome(Renderer2D renderer, float drawX, float drawY, float width, float height, float alpha) { - if (renderer == null || alpha <= 0.01f) return; - renderNativeShell(renderer, drawX, drawY, width, height, alpha); - renderNativeHeaderFooterBlocks(renderer, drawX, drawY, width, alpha); - renderNativeRows(renderer, drawX, drawY, width, alpha); - } - - private void renderNativeShell(Renderer2D renderer, float x, float y, float w, float h, float alpha) { - renderer.roundedRectShadow(x, y, w, h, 14.5f, 8f, 15f, - HudRenderUtil.scaleAlpha(0xFF000000, 0.30f * alpha)); - renderer.roundedRectSoftShadow(x + 4f, y + 4f, Math.max(0f, w - 8f), Math.max(0f, h - 2f), 12.5f, 12f, 0.20f, - HudRenderUtil.scaleAlpha(uiAccentGlow, 0.16f * alpha)); - renderer.roundedRectGlow(x + 2f, y + 1.5f, Math.max(0f, w - 4f), Math.max(0f, h - 3f), 13f, 0f, 8f, - HudRenderUtil.scaleAlpha(uiAccentGlow, 0.14f * alpha)); - renderer.roundedRectGradientQuad(x, y, w, h, 14.5f, - HudRenderUtil.scaleAlpha(uiFillHotspot, 0.96f * alpha), - HudRenderUtil.scaleAlpha(uiFillTop, 0.96f * alpha), - HudRenderUtil.scaleAlpha(uiFillBottom, 0.97f * alpha), - HudRenderUtil.scaleAlpha(uiFillBottom, 0.97f * alpha)); - renderer.radialGlowMasked(x, y, w, h, 14.5f, 0f, Math.max(90f, w * 0.30f), - x + w * 0.18f, y + 9f, HudRenderUtil.scaleAlpha(uiAccentGlow, 0.26f * alpha)); - renderer.radialGlowMasked(x, y, w, h, 14.5f, 0f, Math.max(70f, w * 0.22f), - x + w * 0.86f, y + h - 8f, HudRenderUtil.scaleAlpha(uiAccentSoft, 0.16f * alpha)); - renderer.roundedRectStrokeGradient(x, y, w, h, 14.5f, 1.25f, - HudRenderUtil.scaleAlpha(uiStroke, 0.82f * alpha), - HudRenderUtil.scaleAlpha(uiStrokeSoft, 0.48f * alpha), - 115f); - - float accentW = Math.max(62f, w * 0.34f); - renderer.roundedRectGradient(x + 21f, y + 11f, accentW, 1.45f, 0.8f, - HudRenderUtil.scaleAlpha(uiAccent, 0.58f * alpha), - HudRenderUtil.scaleAlpha(uiAccentSoft, 0.10f * alpha), - 0f); - renderer.roundedRectGradient(x + w - 14f - accentW * 0.72f, y + h - 15.5f, accentW * 0.62f, 2.5f, 1.25f, - HudRenderUtil.scaleAlpha(uiAccentSoft, 0.06f * alpha), - HudRenderUtil.scaleAlpha(uiAccent, 0.25f * alpha), - 0f); - } - - private void renderNativeHeaderFooterBlocks(Renderer2D renderer, float baseX, float baseY, float width, float alpha) { - if (!snapshot.headerLines().isEmpty() && snapshot.headerHeight() > 0f) { - float x = baseX + 14f; - float y = baseY + snapshot.headerTop(); - float w = width - 28f; - float h = snapshot.headerHeight(); - renderer.roundedRectGradientQuad(x, y, w, h, 10.5f, - HudRenderUtil.scaleAlpha(uiRowHighlight, 0.78f * alpha), - HudRenderUtil.scaleAlpha(uiRowHighlight, 0.52f * alpha), - HudRenderUtil.scaleAlpha(uiRowFill, 0.48f * alpha), - HudRenderUtil.scaleAlpha(uiRowFill, 0.55f * alpha)); - renderer.roundedRectStrokeGradient(x, y, w, h, 10.5f, 0.95f, - HudRenderUtil.scaleAlpha(uiStroke, 0.44f * alpha), - HudRenderUtil.scaleAlpha(uiStrokeSoft, 0.18f * alpha), - 96f); - renderer.roundedRectGradient(x + 12f, y + h - 5.5f, Math.max(30f, w * 0.20f), 1.25f, 0.65f, - HudRenderUtil.scaleAlpha(uiAccent, 0.44f * alpha), - HudRenderUtil.scaleAlpha(uiAccentSoft, 0.06f * alpha), - 0f); - } - if (!snapshot.footerLines().isEmpty() && snapshot.footerHeight() > 0f) { - float x = baseX + 14f; - float y = baseY + snapshot.footerTop(); - float w = width - 28f; - float h = snapshot.footerHeight(); - renderer.roundedRectGradientQuad(x, y, w, h, 10f, - HudRenderUtil.scaleAlpha(uiRowFill, 0.34f * alpha), - HudRenderUtil.scaleAlpha(uiRowFill, 0.46f * alpha), - HudRenderUtil.scaleAlpha(uiRowFill, 0.46f * alpha), - HudRenderUtil.scaleAlpha(uiRowFill, 0.34f * alpha)); - renderer.roundedRectStroke(x, y, w, h, 10f, 0f, 0.8f, - HudRenderUtil.scaleAlpha(uiStrokeSoft, 0.30f * alpha)); - float accentW = Math.max(42f, w * 0.24f); - renderer.roundedRectGradient(x + w - accentW - 12f, y + h - 5.5f, accentW, 1.15f, 0.6f, - HudRenderUtil.scaleAlpha(uiAccentSoft, 0.04f * alpha), - HudRenderUtil.scaleAlpha(uiAccent, 0.32f * alpha), - 0f); - } - } - - private void renderNativeRows(Renderer2D renderer, float baseX, float baseY, float drawWidth, float panelAlpha) { - List rows = collectRowRenders(baseX, baseY, drawWidth, panelAlpha); - if (rows.isEmpty()) return; - - // OrderedUiBatcher merges only adjacent compatible backend entries. Rendering a whole row at a time - // alternates shadow/fill/stroke/chip/circle/arc for every player and explodes the draw list. Keep - // the same visual stack, but render the row layer as grouped passes so equal primitive types stay - // adjacent and become real batches. - renderRowShadows(renderer, rows); - renderRowFills(renderer, rows); - renderRowRelationFills(renderer, rows); - renderRowRelationBars(renderer, rows); - renderRowStrokes(renderer, rows); - renderRowHeadPlates(renderer, rows); - renderRowHeadPlateStrokes(renderer, rows); - renderRowDividers(renderer, rows); - renderRowSpectatorBadges(renderer, rows); - renderScoreChipFills(renderer, rows); - renderScoreChipStrokes(renderer, rows); - renderPingChipFills(renderer, rows); - renderPingChipStrokes(renderer, rows); - renderPingChipGlows(renderer, rows); - renderPingChipOuterDots(renderer, rows); - renderPingChipInnerDots(renderer, rows); - renderPingChipDotStrokes(renderer, rows); - renderPingChipArcs(renderer, rows); - } - - private List collectRowRenders(float baseX, float baseY, float drawWidth, float panelAlpha) { - List entries = snapshot.entries(); - if (entries.isEmpty() || panelAlpha <= 0.01f) return List.of(); - - int rowsPerColumn = Math.max(1, snapshot.rows()); - float contentX = contentX(baseX, drawWidth); - float rowTop = snapshot.rowTop(); - float rowHeight = snapshot.rowHeight(); - float columnStride = snapshot.columnWidth() + snapshot.columnGap(); - List out = new java.util.ArrayList<>(entries.size()); - for (int i = 0; i < entries.size(); i++) { - TabListModel.Entry entry = entries.get(i); - TabListAnimator.RowState state = animator.row(entry.id()); - float presence = state.presence; - float rowAlpha = panelAlpha * AnimationUtility.easeOutCubic(presence); - if (rowAlpha <= 0.01f) continue; - - int col = i / rowsPerColumn; - int row = i % rowsPerColumn; - float x = contentX + col * columnStride; - float y = baseY + rowTop + row * rowHeight + (1f - presence) * 10.0f; - float w = snapshot.columnWidth(); - float h = rowHeight - 5f; - int rowFill = (row & 1) == 0 ? uiRowFill : uiRowAltFill; - int relationColor = entry.relationColor() == 0 ? uiAccent : entry.relationColor() | 0xFF000000; - boolean relation = entry.relation() != CategoryType.DEFAULT; - out.add(new RowRender(entry, x, y, w, h, rowAlpha, rowFill, relationColor, relation)); - } - return out; - } - - private void renderRowShadows(Renderer2D renderer, List rows) { - for (RowRender r : rows) { - renderer.roundedRectSoftShadow(r.x + 1.5f, r.y + 2f, r.w - 3f, r.h, 9.5f, 6f, 0.18f, - HudRenderUtil.scaleAlpha(0xFF000000, 0.14f * r.alpha)); - } - } - - private void renderRowFills(Renderer2D renderer, List rows) { - for (RowRender r : rows) { - renderer.roundedRectGradientQuad(r.x, r.y, r.w, r.h, 9.5f, - HudRenderUtil.scaleAlpha(uiRowHighlight, r.alpha), - HudRenderUtil.scaleAlpha(r.rowFill, r.alpha), - HudRenderUtil.scaleAlpha(r.rowFill, r.alpha), - HudRenderUtil.scaleAlpha(uiRowHighlight, r.alpha)); - } - } - - private void renderRowRelationFills(Renderer2D renderer, List rows) { - for (RowRender r : rows) { - if (!r.relation) continue; - int relationFill = HudRenderUtil.scaleAlpha(r.relationColor, 0.16f * r.alpha); - renderer.roundedRectGradientQuad(r.x + 1f, r.y + 1f, r.w - 2f, r.h - 2f, 8.5f, - relationFill, - HudRenderUtil.scaleAlpha(relationFill, 0.42f), - HudRenderUtil.scaleAlpha(relationFill, 0.28f), - HudRenderUtil.scaleAlpha(relationFill, 0.68f)); - } - } - - private void renderRowRelationBars(Renderer2D renderer, List rows) { - for (RowRender r : rows) { - if (!r.relation) continue; - renderer.roundedRectGradient(r.x + 1.8f, r.y + 6f, 2.25f, r.h - 12f, 1.15f, - HudRenderUtil.scaleAlpha(r.relationColor, 0.62f * r.alpha), - HudRenderUtil.scaleAlpha(r.relationColor, 0.18f * r.alpha), - 90f); - } - } - - private void renderRowStrokes(Renderer2D renderer, List rows) { - for (RowRender r : rows) { - int strokeA = r.relation - ? HudRenderUtil.scaleAlpha(r.relationColor, 0.52f * r.alpha) - : HudRenderUtil.scaleAlpha(uiStroke, 0.40f * r.alpha); - renderer.roundedRectStrokeGradient(r.x, r.y, r.w, r.h, 9.5f, r.relation ? 1.25f : 0.85f, - strokeA, - HudRenderUtil.scaleAlpha(uiStrokeSoft, 0.24f * r.alpha), - 92f); - } - } - - private void renderRowHeadPlates(Renderer2D renderer, List rows) { - for (RowRender r : rows) { - float plateX = r.x + ROW_PAD_X - 3.5f; - float plateY = r.y + (r.h - HEAD_SIZE) * 0.5f - 3.5f; - renderer.roundedRectGradient(plateX, plateY, 37f, 37f, 7.75f, - HudRenderUtil.scaleAlpha(uiRowHighlight, 0.72f * r.alpha), - HudRenderUtil.scaleAlpha(uiRowFill, 0.56f * r.alpha), - 92f); - } - } - - private void renderRowHeadPlateStrokes(Renderer2D renderer, List rows) { - for (RowRender r : rows) { - float plateX = r.x + ROW_PAD_X - 3.5f; - float plateY = r.y + (r.h - HEAD_SIZE) * 0.5f - 3.5f; - renderer.roundedRectStroke(plateX, plateY, 37f, 37f, 7.75f, 0f, 0.75f, - HudRenderUtil.scaleAlpha(uiStrokeSoft, 0.30f * r.alpha)); - } - } - - private void renderRowDividers(Renderer2D renderer, List rows) { - for (RowRender r : rows) { - renderer.roundedRectGradient(r.x + ROW_PAD_X + HEAD_SIZE + 10.5f, r.y + 8.5f, 1.15f, Math.max(6f, r.h - 17f), 0.6f, - HudRenderUtil.scaleAlpha(uiAccentSoft, 0.20f * r.alpha), - HudRenderUtil.scaleAlpha(uiDivider, r.alpha), - 90f); - } - } - - private void renderRowSpectatorBadges(Renderer2D renderer, List rows) { - for (RowRender r : rows) { - if (!r.entry.spectator()) continue; - renderer.roundedRectGradient(r.x + r.w - 57f, r.y + 8.5f, 19f, 4.5f, 2.25f, - HudRenderUtil.scaleAlpha(uiMuted, 0.40f * r.alpha), - HudRenderUtil.scaleAlpha(uiMuted, 0.08f * r.alpha), - 0f); - } - } - - private void renderScoreChipFills(Renderer2D renderer, List rows) { - for (RowRender r : rows) { - if (r.entry.scoreText() == null) continue; - ScoreChip chip = scoreChip(r); - renderer.roundedRectGradient(chip.x, chip.y, chip.w, chip.h, 7.25f, - HudRenderUtil.scaleAlpha(uiPingChip, 0.82f * r.alpha), - HudRenderUtil.scaleAlpha(uiPingChip, 0.50f * r.alpha), - 90f); - } - } - - private void renderScoreChipStrokes(Renderer2D renderer, List rows) { - for (RowRender r : rows) { - if (r.entry.scoreText() == null) continue; - ScoreChip chip = scoreChip(r); - renderer.roundedRectStroke(chip.x, chip.y, chip.w, chip.h, 7.25f, 0f, 0.75f, - HudRenderUtil.scaleAlpha(uiStrokeSoft, 0.30f * r.alpha)); - } - } - - private void renderPingChipFills(Renderer2D renderer, List rows) { - for (RowRender r : rows) { - PingChip chip = pingChip(r); - renderer.roundedRectGradient(chip.x, chip.y, chip.w, chip.h, 7.5f, - HudRenderUtil.scaleAlpha(uiPingChip, 0.84f * r.alpha), - HudRenderUtil.scaleAlpha(uiPingChip, 0.52f * r.alpha), - 90f); - } - } - - private void renderPingChipStrokes(Renderer2D renderer, List rows) { - for (RowRender r : rows) { - PingChip chip = pingChip(r); - renderer.roundedRectStrokeGradient(chip.x, chip.y, chip.w, chip.h, 7.5f, 0.8f, - HudRenderUtil.scaleAlpha(uiStrokeSoft, 0.40f * r.alpha), - HudRenderUtil.scaleAlpha(uiStrokeSoft, 0.18f * r.alpha), - 0f); - } - } - - private void renderPingChipGlows(Renderer2D renderer, List rows) { - for (RowRender r : rows) { - PingChip chip = pingChip(r); - renderer.circleSoftShadow(chip.cx, chip.cy, 5.4f, 7f, 0.34f, - HudRenderUtil.scaleAlpha(pingBaseColor(r.entry.latency()), 0.48f * r.alpha)); - } - } - - private void renderPingChipOuterDots(Renderer2D renderer, List rows) { - for (RowRender r : rows) { - PingChip chip = pingChip(r); - renderer.circle(chip.cx, chip.cy, 4.4f, 0.95f, HudRenderUtil.scaleAlpha(pingBaseColor(r.entry.latency()), 0.30f * r.alpha)); - } - } - - private void renderPingChipInnerDots(Renderer2D renderer, List rows) { - for (RowRender r : rows) { - PingChip chip = pingChip(r); - renderer.circle(chip.cx, chip.cy, 3.35f, 0.9f, HudRenderUtil.scaleAlpha(pingBaseColor(r.entry.latency()), r.alpha)); - } - } - - private void renderPingChipDotStrokes(Renderer2D renderer, List rows) { - for (RowRender r : rows) { - PingChip chip = pingChip(r); - renderer.circleStroke(chip.cx, chip.cy, 4.15f, 0.7f, 0.8f, - HudRenderUtil.scaleAlpha(0xFFFFFFFF, 0.36f * r.alpha)); - } - } - - private void renderPingChipArcs(Renderer2D renderer, List rows) { - for (RowRender r : rows) { - PingChip chip = pingChip(r); - int base = pingBaseColor(r.entry.latency()); - renderer.arcStrokeGradient(chip.cx, chip.cy, 5.6f, 1.25f, -140f, 70f, 0.45f, - HudRenderUtil.scaleAlpha(base, 0.68f * r.alpha), - HudRenderUtil.scaleAlpha(0xFFFFFFFF, 0.16f * r.alpha), - 35f); - } - } - - private ScoreChip scoreChip(RowRender r) { - float pingChipW = r.entry.pingChipWidth(); - float occupiedLeft = r.x + r.w - 10.5f - pingChipW - 10f; - float scoreBoxW = r.entry.scoreBoxWidth(); - float scoreBoxH = 23f; - float scoreX = occupiedLeft - scoreBoxW - 8f; - float scoreY = r.y + (r.h - scoreBoxH) * 0.5f; - return new ScoreChip(scoreX, scoreY, scoreBoxW, scoreBoxH); - } - - private PingChip pingChip(RowRender r) { - float chipW = r.entry.pingChipWidth(); - float chipH = 22.5f; - float chipX = r.x + r.w - 10.5f - chipW; - float chipY = r.y + (r.h - chipH) * 0.5f; - float cx = chipX + 10.2f; - float cy = chipY + chipH * 0.5f; - return new PingChip(chipX, chipY, chipW, chipH, cx, cy); - } - - private void renderHeadsAndText(GuiGraphicsExtractor ctx, Renderer2D renderer, float baseX, float baseY, float drawWidth, float panelAlpha) { - float rowTop = snapshot.rowTop(); - float textH = TabRichTextRenderer.height(TEXT_SIZE); - float contentX = contentX(baseX, drawWidth); - renderHeaderText(renderer, baseX, baseY, drawWidth, panelAlpha); - - for (int i = 0; i < snapshot.entries().size(); i++) { - TabListModel.Entry entry = snapshot.entries().get(i); - TabListAnimator.RowState state = animator.row(entry.id()); - float rowAlpha = panelAlpha * AnimationUtility.easeOutCubic(state.presence); - if (rowAlpha <= 0.01f) continue; - - int col = i / Math.max(1, snapshot.rows()); - int row = i % Math.max(1, snapshot.rows()); - float rowX = contentX + col * (snapshot.columnWidth() + snapshot.columnGap()); - float rowY = baseY + rowTop + row * snapshot.rowHeight() + (1f - state.presence) * 10.0f; - float rowH = snapshot.rowHeight() - 5f; - float headX = rowX + ROW_PAD_X; - float headY = rowY + (rowH - HEAD_SIZE) * 0.5f; - int alpha = Math.max(0, Math.min(255, Math.round(255f * rowAlpha))); - - if (entry.skin() != null) { - PlayerHeadRenderer.drawRounded( - ctx, - headX, - headY, - HEAD_SIZE, - 5.25f, - entry.skin(), - new RenderColor(255, 255, 255, alpha), - true, - new RenderColor(255, 255, 255, Math.round(42f * rowAlpha)), - 1.2f, - false - ); - } - } - - renderPingTexts(renderer, baseX, baseY, drawWidth, panelAlpha); - renderScoreTexts(renderer, baseX, baseY, drawWidth, panelAlpha); - renderNameTexts(renderer, baseX, baseY, drawWidth, panelAlpha, textH); - renderFooterText(renderer, baseX, baseY, drawWidth, panelAlpha); - } - - private void renderPingTexts(Renderer2D renderer, float baseX, float baseY, float drawWidth, float panelAlpha) { - float rowTop = snapshot.rowTop(); - float contentX = contentX(baseX, drawWidth); - float pingTextH = TabRichTextRenderer.height(PING_TEXT_SIZE); - for (int i = 0; i < snapshot.entries().size(); i++) { - TabListModel.Entry entry = snapshot.entries().get(i); - TabListAnimator.RowState state = animator.row(entry.id()); - float rowAlpha = panelAlpha * AnimationUtility.easeOutCubic(state.presence); - if (rowAlpha <= 0.01f) continue; - int col = i / Math.max(1, snapshot.rows()); - int row = i % Math.max(1, snapshot.rows()); - float rowX = contentX + col * (snapshot.columnWidth() + snapshot.columnGap()); - float rowY = baseY + rowTop + row * snapshot.rowHeight() + (1f - state.presence) * 10.0f; - float rowH = snapshot.rowHeight() - 5f; - float right = rowX + snapshot.columnWidth() - 10.5f; - float pingChipW = entry.pingChipWidth(); - float pingChipH = 22.5f; - float pingChipX = right - pingChipW; - float pingChipY = rowY + (rowH - pingChipH) * 0.5f; - TabRichTextRenderer.drawPlain(renderer, entry.pingText(), pingChipX + 19.5f, - pingChipY + (pingChipH - pingTextH) * 0.5f - 0.25f, - pingChipW - 22f, PING_TEXT_SIZE, uiText, rowAlpha); - } - } - - private void renderScoreTexts(Renderer2D renderer, float baseX, float baseY, float drawWidth, float panelAlpha) { - float rowTop = snapshot.rowTop(); - float contentX = contentX(baseX, drawWidth); - float scoreTextH = TabRichTextRenderer.height(SCORE_TEXT_SIZE); - for (int i = 0; i < snapshot.entries().size(); i++) { - TabListModel.Entry entry = snapshot.entries().get(i); - if (entry.scoreText() == null) continue; - TabListAnimator.RowState state = animator.row(entry.id()); - float rowAlpha = panelAlpha * AnimationUtility.easeOutCubic(state.presence); - if (rowAlpha <= 0.01f) continue; - int col = i / Math.max(1, snapshot.rows()); - int row = i % Math.max(1, snapshot.rows()); - float rowX = contentX + col * (snapshot.columnWidth() + snapshot.columnGap()); - float rowY = baseY + rowTop + row * snapshot.rowHeight() + (1f - state.presence) * 10.0f; - float rowH = snapshot.rowHeight() - 5f; - float right = rowX + snapshot.columnWidth() - 10.5f; - float occupiedLeft = right - entry.pingChipWidth() - 10f; - float scoreBoxW = entry.scoreBoxWidth(); - float scoreBoxH = 23f; - float scoreX = occupiedLeft - scoreBoxW - 8f; - float scoreY = rowY + (rowH - scoreBoxH) * 0.5f; - TabRichTextRenderer.draw(renderer, entry.scoreText(), scoreX + 8f, - scoreY + (scoreBoxH - scoreTextH) * 0.5f - 0.5f, - entry.scoreWidth() + 2f, SCORE_TEXT_SIZE, uiText, rowAlpha); - } - } - - private void renderNameTexts(Renderer2D renderer, float baseX, float baseY, float drawWidth, float panelAlpha, float textH) { - float rowTop = snapshot.rowTop(); - float contentX = contentX(baseX, drawWidth); - for (int i = 0; i < snapshot.entries().size(); i++) { - TabListModel.Entry entry = snapshot.entries().get(i); - TabListAnimator.RowState state = animator.row(entry.id()); - float rowAlpha = panelAlpha * AnimationUtility.easeOutCubic(state.presence); - if (rowAlpha <= 0.01f) continue; - int col = i / Math.max(1, snapshot.rows()); - int row = i % Math.max(1, snapshot.rows()); - float rowX = contentX + col * (snapshot.columnWidth() + snapshot.columnGap()); - float rowY = baseY + rowTop + row * snapshot.rowHeight() + (1f - state.presence) * 10.0f; - float rowH = snapshot.rowHeight() - 5f; - float right = rowX + snapshot.columnWidth() - 10.5f; - float pingChipX = right - entry.pingChipWidth(); - float occupiedLeft = pingChipX - 10f; - if (entry.scoreText() != null) { - occupiedLeft = occupiedLeft - entry.scoreBoxWidth() - 18f; - } - float textX = rowX + ROW_PAD_X + HEAD_SIZE + 22f; - float textY = rowY + (rowH - textH) * 0.5f - 1.0f; - float textW = Math.max(40f, occupiedLeft - textX); - int baseColor = entry.spectator() ? uiMuted : uiText; - TabRichTextRenderer.draw(renderer, entry.displayName(), textX, textY, textW, TEXT_SIZE, - baseColor, rowAlpha); - } - } - - private void renderHeaderText(Renderer2D renderer, float baseX, float baseY, float drawWidth, float alpha) { - if (snapshot.headerLines().isEmpty() || alpha <= 0.01f) return; - float lineH = 29f; - float y = baseY + snapshot.headerTop() + 8f + (lineH - TabRichTextRenderer.height(HEADER_TEXT_SIZE)) * 0.5f; - for (Component line : snapshot.headerLines()) { - drawCentered(renderer, line, baseX + drawWidth * 0.5f, y, - drawWidth - 56f, HEADER_TEXT_SIZE, 0xFFFFFFFF, alpha); - y += lineH; - } - } - - private void renderFooterText(Renderer2D renderer, float baseX, float baseY, float drawWidth, float alpha) { - if (snapshot.footerLines().isEmpty() || alpha <= 0.01f) return; - float lineH = 25f; - float y = baseY + snapshot.footerTop() + 7f + (lineH - TabRichTextRenderer.height(FOOTER_TEXT_SIZE)) * 0.5f; - for (Component line : snapshot.footerLines()) { - drawCentered(renderer, line, baseX + drawWidth * 0.5f, y, drawWidth - 56f, FOOTER_TEXT_SIZE, 0xFFFFFFFF, alpha); - y += lineH; - } - } - - private float contentX(float baseX, float drawWidth) { - float contentWidth = snapshot.columns() * snapshot.columnWidth() - + Math.max(0, snapshot.columns() - 1) * snapshot.columnGap(); - return baseX + Math.max(SHELL_PAD_X, (drawWidth - contentWidth) * 0.5f); - } - - private void drawCentered(Renderer2D renderer, - Component text, - float centerX, - float y, - float maxWidth, - float size, - int color, - float alpha) { - if (text == null || alpha <= 0.01f || maxWidth <= 0f) return; - float width = Math.min(maxWidth, TabRichTextRenderer.width(text, size)); - TabRichTextRenderer.draw(renderer, text, centerX - width * 0.5f, y, maxWidth, size, color, alpha); - } - - private void renderFallbackShell(Renderer2D renderer, float drawX, float drawY, float width, float height, float alpha) { - if (alpha <= 0.01f) return; - renderer.roundedRect(drawX, drawY, width, height, 14.5f, 0.95f, - HudRenderUtil.scaleAlpha(uiFillTop, 0.96f * alpha)); - renderer.roundedRectStroke(drawX, drawY, width, height, 14.5f, 0.95f, 1.25f, - HudRenderUtil.scaleAlpha(uiStroke, 0.82f * alpha)); - } - - private record RowRender(TabListModel.Entry entry, - float x, - float y, - float w, - float h, - float alpha, - int rowFill, - int relationColor, - boolean relation) { - } - - private record ScoreChip(float x, float y, float w, float h) { - } - - private record PingChip(float x, float y, float w, float h, float cx, float cy) { - } - - private static int pingBaseColor(int ping) { - if (ping < 0) return 0xFF8993A3; - if (ping < 90) return 0xFF52F2A1; - if (ping < 180) return 0xFFFFD166; - return 0xFFFF5A6E; - } - - private void updatePalette() { - if (syncTheme.get()) { - int windowBg = HudRenderUtil.setAlpha(theme().windowBg(), themeAlpha.get()); - uiFillTop = HudRenderUtil.mixRgb(windowBg, 0xFFFFFF, 0.060f); - uiFillBottom = HudRenderUtil.mixRgb(windowBg, 0x000000, 0.105f); - uiFillHotspot = HudRenderUtil.mixColor(uiFillTop, theme().accentSoft(), 0.12f); - uiStroke = theme().windowStroke(); - uiStrokeSoft = theme().strokeSoft(); - uiAccent = theme().accent(); - uiAccentSoft = theme().accentSoft(); - uiAccentGlow = HudRenderUtil.setAlpha(theme().accent(), 255); - uiText = theme().textPrimary(); - uiMuted = theme().textMuted(); - uiRowFill = HudRenderUtil.setAlpha(theme().surface(), 122); - uiRowAltFill = HudRenderUtil.setAlpha(HudRenderUtil.mixRgb(theme().surface(), 0xFFFFFF, 0.026f), 100); - uiRowHighlight = HudRenderUtil.setAlpha(HudRenderUtil.mixRgb(theme().surface(), 0xFFFFFF, 0.070f), 130); - uiPingChip = HudRenderUtil.setAlpha(HudRenderUtil.mixRgb(theme().surface(), 0x000000, 0.05f), 128); - uiDivider = HudRenderUtil.setAlpha(theme().strokeSoft(), 96); - return; - } - - int baseBg = bgColor.getArgb(); - uiFillTop = HudRenderUtil.mixRgb(baseBg, 0xFFFFFF, 0.060f); - uiFillBottom = HudRenderUtil.mixRgb(baseBg, 0x000000, 0.105f); - uiAccent = accentColor.getArgb() | 0xFF000000; - uiAccentSoft = HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(uiAccent, baseBg, 0.35f), 0.55f); - uiAccentGlow = HudRenderUtil.setAlpha(uiAccent, 255); - uiFillHotspot = HudRenderUtil.mixColor(uiFillTop, uiAccentSoft, 0.12f); - uiText = textColor.getArgb() | 0xFF000000; - uiMuted = mutedColor.getArgb() | 0xFF000000; - uiStroke = HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(uiAccent, 0xFFFFFFFF, 0.18f), 0.62f); - uiStrokeSoft = HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(uiMuted, baseBg, 0.42f), 0.42f); - uiRowFill = HudRenderUtil.setAlpha(HudRenderUtil.mixRgb(baseBg, 0xFFFFFF, 0.045f), 122); - uiRowAltFill = HudRenderUtil.setAlpha(HudRenderUtil.mixRgb(baseBg, 0xFFFFFF, 0.024f), 100); - uiRowHighlight = HudRenderUtil.setAlpha(HudRenderUtil.mixRgb(baseBg, 0xFFFFFF, 0.075f), 130); - uiPingChip = HudRenderUtil.setAlpha(HudRenderUtil.mixRgb(baseBg, 0x000000, 0.06f), 128); - uiDivider = HudRenderUtil.setAlpha(uiMuted, 72); - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tooltips/BetterTooltips.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tooltips/BetterTooltips.java deleted file mode 100644 index 88364db..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tooltips/BetterTooltips.java +++ /dev/null @@ -1,960 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.features.gui.hud.nondraggable.impl.tooltips; - -import dev.loki.lovisual.features.theme.Theme; -import dev.loki.lovisual.config.values.collection.ConfigValue; -import dev.loki.lovisual.config.values.collection.ItemIdSetValue; -import dev.loki.lovisual.config.values.color.KeyBindValue; -import dev.loki.lovisual.config.values.mode.BindMode; -import dev.loki.lovisual.config.values.mode.ModeValue; -import dev.loki.lovisual.config.values.primitive.BooleanValue; -import dev.loki.lovisual.config.values.primitive.NumberValue; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipPositioner; -import net.minecraft.client.gui.screens.inventory.tooltip.DefaultTooltipPositioner; -import net.minecraft.core.NonNullList; -import net.minecraft.core.component.DataComponents; -import net.minecraft.util.FormattedCharSequence; -import net.minecraft.world.item.BlockItem; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.component.ItemContainerContents; -import net.minecraft.world.level.block.ShulkerBoxBlock; -import org.joml.Vector2ic; -import dev.loki.lovisual.config.definition.SettingDef; -import dev.loki.lovisual.features.gui.hud.base.AbstractHudElement; -import dev.loki.lovisual.features.gui.hud.registry.HudElementRegister; -import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; -import dev.loki.lovisual.features.gui.hud.script.HudScriptLayouts; -import dev.loki.lovisual.features.theme.Themes; -import dev.loki.lovisual.render.engine.color.RenderColor; -import dev.loki.lovisual.render.engine.core.context.ViewportContext; -import dev.loki.lovisual.render.engine.profiler.config.ProfilerPhase; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.runtime.UiRuntime; -import dev.loki.lovisual.render.engine.renderer.ui.runtime.render.context.UiProjectionMode; -import dev.loki.lovisual.render.engine.renderer.ui.runtime.render.context.UiRenderContext; -import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.runtime.CachedUiScriptRuntime; -import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.module.UiScriptModule; -import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.module.UiScriptModuleHandle; -import dev.loki.lovisual.render.engine.text.font.info.FontInfo; -import dev.loki.lovisual.render.engine.text.font.Fonts; -import dev.loki.lovisual.render.engine.text.render.TextRenderer; -import dev.loki.lovisual.util.input.KeyManager; -import dev.loki.lovisual.util.item.enchant.TopEnchantUtil; - -import java.util.LinkedHashMap; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import dev.loki.lovisual.features.gui.hud.nondraggable.impl.buttons.BetterButtons; - -@HudElementRegister(order = 20) -public final class BetterTooltips extends AbstractHudElement { - - private static final String[] TOOLTIP_FONT_OPTIONS = { - "Onest", "OnestMedium", "OnestLight", "Inter", "InterMedium", "Comfortaa", "Iosevka", "ProFont", "Monsterrat" - }; - public static final BetterTooltips INSTANCE = new BetterTooltips(); - public static final String GROUP_TOOLTIP_GUI = "vanilla_tooltip_gui"; - public static final String GROUP_TOOLTIP_ITEMS = "vanilla_tooltip_items"; - public static final String GROUP_SHULKER_PREVIEW = "vanilla_shulker_preview"; - private static final String TOOLTIP_PANEL_LAYOUT_ID = "lovisual:api/hud/static/tooltip_panel"; - private static final int TOOLTIP_PAD_X = 4; - private static final int TOOLTIP_PAD_Y = 3; - private static final float TOOLTIP_STROKE_WIDTH = 0.55f; - private static final float TOOLTIP_GLOW_SIZE = 3.75f; - private static final int TOOLTIP_BG_ALPHA = 0xEE; - private static final int TOOLTIP_STROKE_ALPHA = 0xDC; - private static final int TOOLTIP_GLOW_ALPHA = 0x5C; - private static final float TOOLTIP_TEXT_SCALE = 0.35f; - private static final int TOOLTIP_LINE_GAP = 0; - private static final int TOOLTIP_PREVIEW_GAP = 3; - private static final int TOOLTIP_TITLE_GAP = 2; - private static final int SHULKER_HINT_COLOR = 0xFFFFAA00; - private static final float SHULKER_BG_RADIUS = 1.5f; - private static final float SHULKER_BG_SOFTNESS = 0.45f; - private static final int SHULKER_SLOT_BG = 0xFF1A1C20; - private static final int SHULKER_SLOT_BORDER = 0x8A34383E; - private static final float SHULKER_SLOT_RADIUS = 2.0f; - private static final float SHULKER_SLOT_SOFTNESS = 0.65f; - private static final float SHULKER_SLOT_GAP = 2.0f; - private static final float SHULKER_PADDING = 4.0f; - private static List TOOLTIP_LINES; - private static ClientTooltipPositioner TOOLTIP_POSITIONER; - private static int TOOLTIP_MOUSE_X; - private static int TOOLTIP_MOUSE_Y; - private static ItemStack TOOLTIP_STACK; - private static ItemStack LAST_TOOLTIP_STACK; - private static GuiGraphicsExtractor TOOLTIP_CTX; - private static float TOOLTIP_SCALE_OVERRIDE = 1.0f; - private static PreviewLayout SHULKER_CACHE; - private static int SHULKER_CACHE_SIGNATURE; - private static float SHULKER_CACHE_PAD; - private static float SHULKER_CACHE_SLOT_SIZE; - private static float SHULKER_CACHE_SLOT_GAP; - private static boolean SHULKER_CACHE_HAS_ITEMS; - private final BooleanValue itemTooltipEnabled = bool("item_tooltip", true); - private final BooleanValue shulkerPreviewEnabled = bool("shulker_preview", true); - private final BooleanValue itemInfoColorize = bool("item_info_colorize", true); - private final ModeValue tooltipFont = mode("tooltip_font", "Iosevka", TOOLTIP_FONT_OPTIONS); - private final NumberValue tooltipTextSize = - num("tooltip_text_size", 1.0f, 0.5f, 2.0f); - private final ItemIdSetValue topIgnore = registerTopIgnore(); - private final NumberValue shulkerPreviewSlotSize = - visibleWhen(num("shulker_slot_size", 18.0f, 12.0f, 24.0f), this::isShulkerPreviewEnabled); - private final KeyBindValue shulkerPreviewHold = - visibleWhen(bind("shulker_preview_hold", "LEFT_SHIFT", BindMode.HOLD), this::isShulkerPreviewEnabled); - private final UiScriptModuleHandle tooltipPanelHandle = HudScriptLayouts.handle(TOOLTIP_PANEL_LAYOUT_ID); - private final CachedUiScriptRuntime tooltipRuntime = new CachedUiScriptRuntime(HudScriptLayouts.runtimeReporter()); - private BetterTooltips() { - super("vanilla_tooltips", "Tooltips", true); - } - - public static BetterTooltips get() { - return INSTANCE; - } - - public static boolean isGroupId(String id) { - return SettingsGroup.fromId(id) != null; - } - - public static void beginTooltipFrame() { - TOOLTIP_LINES = null; - TOOLTIP_POSITIONER = null; - TOOLTIP_STACK = null; - TOOLTIP_CTX = null; - LAST_TOOLTIP_STACK = null; - TOOLTIP_SCALE_OVERRIDE = 1.0f; - } - - public static boolean hasTooltip() { - return TOOLTIP_LINES != null && !TOOLTIP_LINES.isEmpty(); - } - - public static void captureTooltipOrdered(List lines, - ClientTooltipPositioner positioner, - int mouseX, int mouseY) { - if (lines == null || lines.isEmpty()) return; - TOOLTIP_LINES = convertOrdered(lines); - TOOLTIP_POSITIONER = positioner != null ? positioner : DefaultTooltipPositioner.INSTANCE; - TOOLTIP_MOUSE_X = mouseX; - TOOLTIP_MOUSE_Y = mouseY; - TOOLTIP_STACK = ItemStack.EMPTY; - } - - public static void captureItemTooltipOrdered(List lines, - ClientTooltipPositioner positioner, - int mouseX, int mouseY, - ItemStack stack, - GuiGraphicsExtractor ctx) { - if (lines == null || lines.isEmpty()) return; - TOOLTIP_LINES = convertOrdered(lines); - TOOLTIP_POSITIONER = positioner != null ? positioner : DefaultTooltipPositioner.INSTANCE; - TOOLTIP_MOUSE_X = mouseX; - TOOLTIP_MOUSE_Y = mouseY; - TOOLTIP_STACK = stack; - TOOLTIP_CTX = ctx; - } - - public static void setDrawContext(GuiGraphicsExtractor ctx) { - TOOLTIP_CTX = ctx; - } - - public static void setLastTooltipStack(ItemStack stack) { - LAST_TOOLTIP_STACK = stack; - } - - public static ItemStack consumeLastTooltipStack() { - ItemStack stack = LAST_TOOLTIP_STACK; - LAST_TOOLTIP_STACK = null; - return stack; - } - - public static void renderTooltip() { - if (!hasTooltip()) return; - renderTooltipInternal( - TOOLTIP_LINES, - TOOLTIP_POSITIONER, - TOOLTIP_MOUSE_X, - TOOLTIP_MOUSE_Y - ); - } - - public static void setTooltipScaleOverride(float scale) { - TOOLTIP_SCALE_OVERRIDE = Math.max(0.5f, Math.min(1.5f, scale)); - } - - public static void resetTooltipScaleOverride() { - TOOLTIP_SCALE_OVERRIDE = 1.0f; - } - - public static void renderTooltipWithContext(GuiGraphicsExtractor ctx) { - if (ctx != null && TOOLTIP_CTX == null) { - TOOLTIP_CTX = ctx; - } - renderTooltip(); - } - - private static void renderTooltipInternal(List lines, - ClientTooltipPositioner positioner, - int mouseX, int mouseY) { - Minecraft mc = Minecraft.getInstance(); - if (mc == null) return; - - TextRenderer tr = Fonts.renderer(INSTANCE.tooltipFont.get(), FontInfo.Type.Regular, TextRenderer.get()); - if (tr == null) return; - - PreviewLayout preview = buildShulkerPreviewLayout(); - boolean previewOpen = preview != null; - List finalLines = prepareTooltipLines(lines, previewOpen); - - float textScale = TOOLTIP_TEXT_SCALE * TOOLTIP_SCALE_OVERRIDE * INSTANCE.tooltipTextSize.get(); - tr.begin(textScale, false, false); - int maxWidth = 0; - for (TooltipLine line : finalLines) { - maxWidth = Math.max(maxWidth, (int) Math.ceil(tr.getWidth(line.text, false))); - } - int lineHeight = (int) Math.ceil(tr.getHeight(false)); - tr.end(); - - if (finalLines.isEmpty()) { - finalLines = List.of(new TooltipLine(" ", Theme.theme().textPrimary())); - } - - int textHeight = finalLines.size() * lineHeight; - if (finalLines.size() > 1) { - textHeight += TOOLTIP_TITLE_GAP; - textHeight += Math.max(0, finalLines.size() - 2) * TOOLTIP_LINE_GAP; - } - int previewW = preview != null ? preview.width : 0; - int previewH = preview != null ? preview.height : 0; - int contentWidth = Math.max(maxWidth, previewW); - int totalW = contentWidth + TOOLTIP_PAD_X * 2; - int totalH = TOOLTIP_PAD_Y * 2 + textHeight; - if (preview != null) { - totalH += TOOLTIP_PREVIEW_GAP + previewH; - } - - int x = mouseX; - int y = mouseY; - if (positioner != null) { - Vector2ic pos = positioner.positionTooltip( - mc.getWindow().getGuiScaledWidth(), - mc.getWindow().getGuiScaledHeight(), - mouseX, mouseY, - totalW, totalH - ); - x = pos.x(); - y = pos.y(); - } - - Themes.Theme theme = Theme.theme(); - int bg = HudRenderUtil.setAlpha(theme.windowBg(), TOOLTIP_BG_ALPHA); - int edgeBase = HudRenderUtil.mixColor(theme.accentSoft(), theme.accent(), 0.34f); - int glowColor = HudRenderUtil.setAlpha(edgeBase, TOOLTIP_GLOW_ALPHA); - int strokeTopLeft = HudRenderUtil.setAlpha(HudRenderUtil.mixColor(theme.accentSoft(), theme.accent(), 0.14f), TOOLTIP_STROKE_ALPHA); - int strokeTopRight = HudRenderUtil.setAlpha(HudRenderUtil.mixColor(theme.accentSoft(), theme.accent(), 0.82f), TOOLTIP_STROKE_ALPHA); - int strokeBottomRight = HudRenderUtil.setAlpha(HudRenderUtil.mixColor(theme.accentSoft(), theme.accent(), 0.58f), TOOLTIP_STROKE_ALPHA); - int strokeBottomLeft = HudRenderUtil.setAlpha(HudRenderUtil.mixColor(theme.accentSoft(), theme.accent(), 0.24f), TOOLTIP_STROKE_ALPHA); - - ViewportContext.beginScaled(TOOLTIP_CTX); - boolean panelRendered = INSTANCE.renderTooltipPanelScripted( - mc, - Renderer2D.COLOR, - tr, - x, - y, - totalW, - totalH, - bg, - strokeTopLeft, - strokeTopRight, - strokeBottomRight, - strokeBottomLeft, - glowColor, - TOOLTIP_STROKE_WIDTH, - TOOLTIP_GLOW_SIZE, - 1.0f - ); - if (!panelRendered) { - drawTooltipPanelFallback(x, y, totalW, totalH, bg, - strokeTopLeft, strokeTopRight, strokeBottomRight, strokeBottomLeft, - glowColor); - } - - int textX = x + TOOLTIP_PAD_X; - int textY = y + TOOLTIP_PAD_Y; - tr.begin(textScale, false, false); - for (int i = 0; i < finalLines.size(); i++) { - TooltipLine line = finalLines.get(i); - tr.render(line.text, textX, textY, new RenderColor(line.color), true); - textY += lineHeight; - if (i == 0 && finalLines.size() > 1) { - textY += TOOLTIP_TITLE_GAP; - } else if (i + 1 < finalLines.size()) { - textY += TOOLTIP_LINE_GAP; - } - } - tr.end(); - ViewportContext.end(TOOLTIP_CTX); - - if (preview != null) { - int contentW = totalW - TOOLTIP_PAD_X * 2; - int previewX = x + TOOLTIP_PAD_X + Math.max(0, (contentW - preview.width) / 2); - int previewY = y + TOOLTIP_PAD_Y + textHeight + TOOLTIP_PREVIEW_GAP; - renderShulkerPreview(preview, previewX, previewY); - } - } - - private static String hex(int argb) { - return String.format("#%08X", argb); - } - - private static void drawTooltipPanelFallback(float x, float y, float width, float height, - int bg, - int strokeTopLeft, - int strokeTopRight, - int strokeBottomRight, - int strokeBottomLeft, - int glow) { - float scale = ViewportContext.getScaleFactor(); - float xb = x * scale; - float yb = y * scale; - float wb = width * scale; - float hb = height * scale; - float strokeWidth = TOOLTIP_STROKE_WIDTH * scale; - ViewportContext.unscaledProjection(); - Renderer2D.COLOR.roundedRectGlow( - xb, yb, wb, hb, - 0.0f, - 0.0f, - TOOLTIP_GLOW_SIZE * scale, - glow - ); - Renderer2D.COLOR.quad(xb, yb, wb, hb, bg); - drawQuadStrokeGradient(Renderer2D.COLOR, xb, yb, wb, hb, strokeWidth, - strokeTopLeft, strokeTopRight, strokeBottomRight, strokeBottomLeft); - ViewportContext.scaledProjection(); - } - - private static void drawQuadStrokeGradient(Renderer2D renderer, - float x, float y, float width, float height, - float thickness, - int cTopLeft, int cTopRight, - int cBottomRight, int cBottomLeft) { - if (renderer == null || thickness <= 0.0f || width <= 0.0f || height <= 0.0f) return; - float t = Math.min(thickness, Math.min(width, height) * 0.5f); - renderer.quadGradient(x, y, width, t, cTopLeft, cTopRight, cTopRight, cTopLeft); - renderer.quadGradient(x, y + height - t, width, t, cBottomLeft, cBottomRight, cBottomRight, cBottomLeft); - float innerHeight = Math.max(0.0f, height - t * 2.0f); - if (innerHeight > 0.0f) { - renderer.quadGradient(x, y + t, t, innerHeight, cTopLeft, cTopLeft, cBottomLeft, cBottomLeft); - renderer.quadGradient(x + width - t, y + t, t, innerHeight, cTopRight, cTopRight, cBottomRight, cBottomRight); - } - } - - private static List convertOrdered(List lines) { - List out = new ArrayList<>(); - int fallback = Theme.theme().textPrimary(); - for (FormattedCharSequence line : lines) { - StringBuilder sb = new StringBuilder(); - int[] color = new int[]{0}; - line.accept((idx, style, codePoint) -> { - sb.appendCodePoint(codePoint); - if (color[0] == 0 && style != null && style.getColor() != null) { - color[0] = 0xFF000000 | style.getColor().getValue(); - } - return true; - }); - if (sb.length() == 0) sb.append(' '); - int c = color[0] != 0 ? color[0] : fallback; - out.add(new TooltipLine(sb.toString(), c)); - } - return out; - } - - private static List prepareTooltipLines(List lines, boolean previewOpen) { - if (lines == null || lines.isEmpty()) return lines; - boolean isShulker = isShulkerBox(TOOLTIP_STACK); - boolean showHint = shouldShowShulkerHint(isShulker); - - if (!previewOpen && !showHint) { - return lines; - } - - List out = new ArrayList<>(lines.size() + (showHint ? 1 : 0)); - for (TooltipLine line : lines) { - if (previewOpen && isShulker && (isShulkerMoreLine(line.text) || isShulkerItemLine(line.text))) { - continue; - } - out.add(line); - } - - if (showHint) { - out.add(new TooltipLine(buildShulkerHint(), SHULKER_HINT_COLOR)); - } - return out; - } - - private static boolean shouldShowShulkerHint(boolean isShulker) { - if (!isShulker) return false; - if (!INSTANCE.isShulkerPreviewEnabled()) return false; - if (!hasShulkerContents()) return false; - return !INSTANCE.isShulkerPreviewHeld(); - } - - private static String buildShulkerHint() { - String bind = INSTANCE.getShulkerPreviewBind(); - return "<<" + bind + ">> показать содержимое"; - } - - private static boolean isShulkerMoreLine(String text) { - if (text == null) return false; - String t = text.trim(); - if (t.isEmpty()) return false; - String lower = t.toLowerCase(Locale.ROOT); - if (lower.startsWith("and ") || lower.startsWith("и ")) { - return t.contains("...") || t.contains("…"); - } - if (lower.startsWith("и еще") || lower.startsWith("и ещё")) { - return t.contains("...") || t.contains("…"); - } - return false; - } - - private static boolean isShulkerItemLine(String text) { - if (text == null) return false; - String t = text.trim(); - if (t.isEmpty()) return false; - int l = t.lastIndexOf('('); - int r = t.lastIndexOf(')'); - if (l < 0 || r <= l) return false; - for (int i = l + 1; i < r; i++) { - char c = t.charAt(i); - if (c >= '0' && c <= '9') { - return true; - } - } - return false; - } - - private static boolean hasShulkerContents() { - if (!isShulkerBox(TOOLTIP_STACK)) return false; - ItemContainerContents container = TOOLTIP_STACK.get(DataComponents.CONTAINER); - if (container == null) return false; - for (net.minecraft.world.item.ItemStackTemplate template : container.nonEmptyItems()) { - ItemStack stack = template.create(); - if (stack != null && !stack.isEmpty()) { - return true; - } - } - return false; - } - - private static boolean hasAnyStacks(NonNullList stacks) { - if (stacks == null) return false; - for (ItemStack s : stacks) { - if (s != null && !s.isEmpty()) { - return true; - } - } - return false; - } - - private static PreviewLayout buildShulkerPreviewLayout() { - BetterTooltips shulker = INSTANCE; - if (!shulker.isShulkerPreviewEnabled()) { - return null; - } - if (!shulker.isShulkerPreviewHeld()) { - return null; - } - if (TOOLTIP_STACK == null || TOOLTIP_STACK.isEmpty()) { - return null; - } - if (!isShulkerBox(TOOLTIP_STACK)) { - return null; - } - if (TOOLTIP_CTX == null) { - return null; - } - - ItemContainerContents container = TOOLTIP_STACK.get(DataComponents.CONTAINER); - if (container == null) { - return null; - } - - int cols = 9; - int rows = 3; - NonNullList stacks = NonNullList.withSize(cols * rows, ItemStack.EMPTY); - container.copyInto(stacks); - - float pad = SHULKER_PADDING; - float slotSize = shulker.shulkerPreviewSlotSize(); - float slotGap = SHULKER_SLOT_GAP; - int containerSignature = computeShulkerSignature(TOOLTIP_STACK, stacks); - - if (SHULKER_CACHE != null - && SHULKER_CACHE_SIGNATURE == containerSignature - && Float.compare(SHULKER_CACHE_PAD, pad) == 0 - && Float.compare(SHULKER_CACHE_SLOT_SIZE, slotSize) == 0 - && Float.compare(SHULKER_CACHE_SLOT_GAP, slotGap) == 0) { - return SHULKER_CACHE_HAS_ITEMS ? SHULKER_CACHE : null; - } - - boolean hasItems = hasAnyStacks(stacks); - SHULKER_CACHE_SIGNATURE = containerSignature; - SHULKER_CACHE_PAD = pad; - SHULKER_CACHE_SLOT_SIZE = slotSize; - SHULKER_CACHE_SLOT_GAP = slotGap; - SHULKER_CACHE_HAS_ITEMS = hasItems; - if (!hasItems) { - SHULKER_CACHE = null; - return null; - } - - int bgW = Math.round(pad * 2f + cols * slotSize + (cols - 1) * slotGap); - int bgH = Math.round(pad * 2f + rows * slotSize + (rows - 1) * slotGap); - - SHULKER_CACHE = new PreviewLayout( - bgW, - bgH, - cols, - rows, - pad, - slotSize, - slotGap, - copyStacks(stacks), - containerSignature - ); - return SHULKER_CACHE; - } - - private static void renderShulkerPreview(PreviewLayout preview, int baseX, int baseY) { - if (preview == null) return; - - Minecraft mc = Minecraft.getInstance(); - if (mc == null || mc.getWindow() == null) return; - - int slotBg = SHULKER_SLOT_BG; - int border = SHULKER_SLOT_BORDER; - - float scale = ViewportContext.getScaleFactor(); - - ViewportContext.beginUnscaled(TOOLTIP_CTX); - Renderer2D.COLOR.begin(); - - float slotStroke = 0.35f * scale; - - for (int row = 0; row < preview.rows; row++) { - for (int col = 0; col < preview.cols; col++) { - float sx = baseX + preview.pad + col * (preview.slotSize + preview.slotGap); - float sy = baseY + preview.pad + row * (preview.slotSize + preview.slotGap); - float sxb = sx * scale; - float syb = sy * scale; - float swb = preview.slotSize * scale; - float shb = preview.slotSize * scale; - - Renderer2D.COLOR.roundedRect(sxb, syb, swb, shb, - SHULKER_SLOT_RADIUS * scale, - SHULKER_SLOT_SOFTNESS * scale, - slotBg); - Renderer2D.COLOR.roundedRectStroke(sxb, syb, swb, shb, - SHULKER_SLOT_RADIUS * scale, - SHULKER_SLOT_SOFTNESS * scale, - slotStroke, - border); - } - } - Renderer2D.COLOR.render(); - ViewportContext.end(TOOLTIP_CTX); - - if (TOOLTIP_CTX == null) return; - - // Slot geometry uses raw framebuffer coordinates, so item icons use the same - // projection and scale to remain centered in the grid. - ViewportContext.beginUnscaled(TOOLTIP_CTX); - Renderer2D.COLOR.begin(); - float offset = (preview.slotSize - 16f) * 0.5f; - try (ProfilerPhase.Scope ignored = ProfilerPhase.scope("2d:tooltip:shulker_preview")) { - int seed = 1; - int index = 0; - for (int row = 0; row < preview.rows; row++) { - for (int col = 0; col < preview.cols; col++) { - if (index >= preview.stacks.size()) { - Renderer2D.COLOR.render(); - ViewportContext.end(TOOLTIP_CTX); - return; - } - ItemStack stack = preview.stacks.get(index); - if (stack == null || stack.isEmpty()) { - index++; - continue; - } - - float sx = (baseX + preview.pad + col * (preview.slotSize + preview.slotGap) + offset) * scale; - float sy = (baseY + preview.pad + row * (preview.slotSize + preview.slotGap) + offset) * scale; - Renderer2D.COLOR.item(stack, sx, sy, scale, seed++, Renderer2D.ITEM_OVERLAY_ALL, null); - index++; - } - } - Renderer2D.COLOR.render(); - ViewportContext.end(TOOLTIP_CTX); - } - } - - private static boolean isShulkerBox(ItemStack stack) { - if (stack == null || stack.isEmpty()) return false; - if (!(stack.getItem() instanceof BlockItem blockItem)) return false; - return blockItem.getBlock() instanceof ShulkerBoxBlock; - } - - private static NonNullList copyStacks(NonNullList stacks) { - NonNullList copy = NonNullList.withSize(stacks.size(), ItemStack.EMPTY); - for (int i = 0; i < stacks.size(); i++) { - ItemStack stack = stacks.get(i); - copy.set(i, stack == null ? ItemStack.EMPTY : stack.copy()); - } - return copy; - } - - private static int computeShulkerSignature(ItemStack tooltipStack, NonNullList stacks) { - int hash = 1; - hash = 31 * hash + (tooltipStack == null ? 0 : tooltipStack.getItem().hashCode()); - hash = 31 * hash + stacks.size(); - for (int i = 0; i < stacks.size(); i++) { - ItemStack stack = stacks.get(i); - if (stack == null || stack.isEmpty()) { - hash = 31 * hash; - continue; - } - hash = 31 * hash + stack.getItem().hashCode(); - hash = 31 * hash + stack.getCount(); - hash = 31 * hash + stack.getDamageValue(); - hash = 31 * hash + stack.getComponents().hashCode(); - } - return hash; - } - - @Override - protected void defineSettings(List defs) { - defs.add(0, enabledSettingDef()); - } - - @Override - protected void onLoaded() { - registerBind(shulkerPreviewHold); - } - - public List getSettingsGroup(String groupId) { - SettingsGroup group = SettingsGroup.fromId(groupId); - if (group == null) return List.of(); - List defs = new ArrayList<>(); - switch (group) { - case TOOLTIP_GUI -> defs.add(enabledSettingDef()); - case TOOLTIP_ITEMS -> add(defs, itemTooltipEnabled, itemInfoColorize, topIgnore); - case SHULKER_PREVIEW -> addShulkerPreviewSettings(defs); - } - return defs; - } - - private ItemIdSetValue registerTopIgnore() { - ItemIdSetValue value = TopEnchantUtil.ignoreValue(); - return declareSetting(value, SettingDef.textList(value)); - } - - private void addShulkerPreviewSettings(List defs) { - add(defs, - shulkerPreviewEnabled, - shulkerPreviewSlotSize, - shulkerPreviewHold - ); - } - - private void add(List defs, ConfigValue... values) { - for (ConfigValue value : values) { - SettingDef def = settingDef(value); - if (def != null) { - defs.add(def); - } - } - } - - public boolean useCustomGuiTooltips() { - - BetterButtons buttons = BetterButtons.get(); - if (buttons != null && buttons.useUiButtons()) return true; - return isEnabled(); - } - - public boolean useCustomItemTooltips() { - - BetterButtons buttons = BetterButtons.get(); - if (buttons != null && buttons.useUiButtons()) return true; - return itemTooltipEnabled.get(); - } - - public boolean isItemInfoColorizeEnabled() { - return itemInfoColorize.get(); - } - - public boolean isTooltipAlphaEnabled() { - return anyTooltipsEnabled(); - } - - public int tooltipColor() { - return (TOOLTIP_BG_ALPHA << 24) | 0x00101114; - } - - public boolean isShulkerPreviewEnabled() { - return shulkerPreviewEnabled.get(); - } - - public void setShulkerPreviewEnabled(boolean value) { - shulkerPreviewEnabled.set(value); - saveConfig(); - } - - public boolean isGuiTooltipEnabled() { - return isEnabled(); - } - - public void setGuiTooltipEnabled(boolean value) { - setEnabled(value); - } - - public boolean isItemTooltipEnabled() { - return itemTooltipEnabled.get(); - } - - public void setItemTooltipEnabled(boolean value) { - itemTooltipEnabled.set(value); - saveConfig(); - } - - public boolean isShulkerPreviewHeld() { - - return KeyManager.isHeldAllowScreen(bindingName(shulkerPreviewHold)); - } - - public String getShulkerPreviewBind() { - return shulkerPreviewHold.get(); - } - - public int shulkerPreviewBgRgb() { - return Theme.theme().windowBg() & 0x00FFFFFF; - } - - public int shulkerPreviewBgAlpha() { - return TOOLTIP_BG_ALPHA; - } - - public float shulkerPreviewBgRadius() { - return SHULKER_BG_RADIUS; - } - - public float shulkerPreviewBgSoftness() { - return SHULKER_BG_SOFTNESS; - } - - public int shulkerPreviewSlotBgRgb() { - return SHULKER_SLOT_BG & 0x00FFFFFF; - } - - public int shulkerPreviewSlotBorderRgb() { - return SHULKER_SLOT_BORDER & 0x00FFFFFF; - } - - public int shulkerPreviewSlotBorderAlpha() { - return (SHULKER_SLOT_BORDER >>> 24) & 0xFF; - } - - public float shulkerPreviewSlotRadius() { - return SHULKER_SLOT_RADIUS; - } - - public float shulkerPreviewSlotSoftness() { - return SHULKER_SLOT_SOFTNESS; - } - - public float shulkerPreviewSlotSize() { - return shulkerPreviewSlotSize.get(); - } - - public float shulkerPreviewSlotGap() { - return SHULKER_SLOT_GAP; - } - - public float shulkerPreviewPadding() { - return SHULKER_PADDING; - } - - private boolean anyTooltipsEnabled() { - - if (useCustomGuiTooltips()) return true; - if (itemTooltipEnabled.get()) return true; - return shulkerPreviewEnabled.get(); - } - - private String bindingName(KeyBindValue value) { - return name() + ":" + value.getName(); - } - - private void registerBind(KeyBindValue value) { - String name = bindingName(value); - KeyManager.unregisterAll(name); - if (!value.isNone()) { - KeyManager.registerCombo(name, value.get()); - } - } - - private boolean renderTooltipPanelScripted(Minecraft mc, - Renderer2D renderer, - TextRenderer textRenderer, - float x, - float y, - float width, - float height, - int bg, - int strokeTopLeft, - int strokeTopRight, - int strokeBottomRight, - int strokeBottomLeft, - int glow, - float strokeWidth, - float glowSize, - float panelAlpha) { - if (mc == null || renderer == null || textRenderer == null || tooltipPanelHandle.isRuntimeBlocked()) - return false; - HudScriptLayouts.pollReloadCombo(mc); - if (tooltipPanelHandle.consumeChanged()) { - resetTooltipPanelRuntime(); - } - UiScriptModule module = ensureTooltipPanelModule(mc); - if (module == null) return false; - - float fbScale = ViewportContext.getScaleFactor(); - float drawX = x * fbScale; - float drawY = y * fbScale; - float drawW = width * fbScale; - float drawH = height * fbScale; - - float strokeWidthPx = strokeWidth * fbScale; - float glowSizePx = glowSize * fbScale; - Map props = tooltipPanelProps( - drawW, drawH, bg, strokeTopLeft, strokeTopRight, strokeBottomRight, strokeBottomLeft, glow, strokeWidthPx, glowSizePx - ); - long signature = CachedUiScriptRuntime.signature(props); - long layoutSignature = CachedUiScriptRuntime.mix( - CachedUiScriptRuntime.mix(signature, drawX), - drawY - ); - UiRuntime baked = tooltipRuntime.bake( - tooltipPanelHandle, - module, - "tooltip_panel", - signature, - layoutSignature, - drawW, - drawH, - textRenderer, - drawX, - drawY, - drawW, - drawH, - () -> props - ); - if (baked == null) return false; - - ViewportContext.unscaledProjection(); - boolean startedBatch = !Renderer2D.isBatching(); - if (startedBatch) renderer.begin(); - baked.render(new UiRenderContext(renderer, textRenderer, TOOLTIP_CTX, 0.0f, UiProjectionMode.RAW_FRAMEBUFFER, panelAlpha)); - if (startedBatch) { - renderer.render(); - } - ViewportContext.scaledProjection(); - return true; - } - - private Map tooltipPanelProps(float drawW, - float drawH, - int bg, - int strokeTopLeft, - int strokeTopRight, - int strokeBottomRight, - int strokeBottomLeft, - int glow, - float strokeWidthPx, - float glowSizePx) { - LinkedHashMap props = new LinkedHashMap<>(12); - props.put("width", drawW); - props.put("height", drawH); - props.put("bg", hex(bg)); - props.put("strokeTopLeft", hex(strokeTopLeft)); - props.put("strokeTopRight", hex(strokeTopRight)); - props.put("strokeBottomRight", hex(strokeBottomRight)); - props.put("strokeBottomLeft", hex(strokeBottomLeft)); - props.put("strokeWidth", strokeWidthPx); - props.put("glow", hex(glow)); - props.put("glowSize", glowSizePx); - return props; - } - - private UiScriptModule ensureTooltipPanelModule(Minecraft mc) { - if (mc == null || mc.getResourceManager() == null) return null; - if (!tooltipPanelHandle.ensureLoaded(mc.getResourceManager())) { - HudScriptLayouts.reportLoadError(tooltipPanelHandle); - return null; - } - tooltipPanelHandle.consumeChanged(); - return tooltipPanelHandle.module(); - } - - private void resetTooltipPanelRuntime() { - tooltipRuntime.reset(); - } - - private enum SettingsGroup { - TOOLTIP_GUI(GROUP_TOOLTIP_GUI), - TOOLTIP_ITEMS(GROUP_TOOLTIP_ITEMS), - SHULKER_PREVIEW(GROUP_SHULKER_PREVIEW); - - private final String id; - - SettingsGroup(String id) { - this.id = id; - } - - private static SettingsGroup fromId(String id) { - if (id == null) return null; - for (SettingsGroup group : values()) { - if (group.id.equals(id)) return group; - } - return null; - } - } - - private record TooltipLine(String text, int color) { - } - - private record PreviewLayout(int width, - int height, - int cols, - int rows, - float pad, - float slotSize, - float slotGap, - NonNullList stacks, - int containerSignature) { - } - -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/script/CompactHudStatModel.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/script/CompactHudStatModel.java deleted file mode 100644 index 937f36b..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/script/CompactHudStatModel.java +++ /dev/null @@ -1,894 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.features.gui.hud.script; - -import java.util.LinkedHashMap; -import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.UiBounds; -import dev.loki.lovisual.render.engine.text.font.Font; - -/** - * Mutable frame snapshot for compact stat widgets such as FPS, TPS, ping, and game time. - */ -public final class CompactHudStatModel { - private static final char[] HEX = "0123456789ABCDEF".toCharArray(); - - private final String id; - private final Background background = new Background(); - private final Icon icon = new Icon(); - private final Divider divider = new Divider(); - private final TextRun value = new TextRun(); - private final TextRun unit = new TextRun(); - private final TextRun extra = new TextRun(); - private final Animation animation = new Animation(); - private final LinkedHashMap data = new LinkedHashMap<>(); - private boolean visible; - private float rootX; - private float rootY; - private float width; - private float height; - private float radius; - private float scale; - - public CompactHudStatModel(String id) { - this.id = id != null ? id : ""; - } - - private static void putBounds(LinkedHashMap patches, - String key, - float x, - float y, - float width, - float height) { - patches.put(key, new UiBounds(x, y, Math.max(0.0f, width), Math.max(0.0f, height))); - } - - private static void putPropPatch(LinkedHashMap> patches, - String key, - String prop, - Object value) { - LinkedHashMap props = patches.get(key); - if (props == null) { - props = new LinkedHashMap<>(2); - patches.put(key, props); - } - props.put(prop, value); - } - - private static void putTextPatch(LinkedHashMap> patches, - String key, - String text, - String color) { - putTextPatch(patches, key, text, color, Float.NaN); - } - - private static void putTextPatch(LinkedHashMap> patches, - String key, - String text, - String color, - float textOffsetY) { - LinkedHashMap props = new LinkedHashMap<>(3); - props.put("text", text != null ? text : ""); - if (color != null) props.put("color", color); - if (!Float.isNaN(textOffsetY)) props.put("textOffsetY", textOffsetY); - patches.put(key, props); - } - - private static long mix(long hash, boolean value) { - return mix(hash, value ? 1 : 0); - } - - private static long mix(long hash, int value) { - hash ^= value; - return hash * 0x100000001b3L; - } - - private static long mix(long hash, float value) { - return mix(hash, Float.floatToIntBits(value)); - } - - private static long mix(long hash, String value) { - return mix(hash, value != null ? value.hashCode() : 0); - } - - private static long mixObject(long hash, Object value) { - if (value instanceof Boolean b) return mix(hash, b); - if (value instanceof Integer i) return mix(hash, i); - if (value instanceof Float f) return mix(hash, f); - if (value instanceof Number n) return mix(hash, Float.floatToIntBits(n.floatValue())); - return mix(hash, value != null ? value.toString() : ""); - } - - private static float clamp01(float value) { - return Math.max(0.0f, Math.min(1.0f, value)); - } - - private static String alpha(String hex, float alpha) { - int argb = parseColor(hex); - int a = Math.round(((argb >>> 24) & 0xFF) * clamp01(alpha)); - return color((argb & 0x00FFFFFF) | (a << 24)); - } - - private static float alpha01(int argb) { - return ((argb >>> 24) & 0xFF) / 255.0f; - } - - private static int parseColor(String hex) { - if (hex == null || hex.isBlank()) return 0xFFFFFFFF; - String text = hex.startsWith("#") ? hex.substring(1) : hex; - try { - if (text.length() == 6) return 0xFF000000 | Integer.parseUnsignedInt(text, 16); - if (text.length() == 8) return (int) Long.parseLong(text, 16); - } catch (NumberFormatException ignored) { - } - return 0xFFFFFFFF; - } - - private static String color(int argb) { - return colorString(argb); - } - - public static String colorString(int argb) { - char[] out = new char[9]; - out[0] = '#'; - for (int i = 7; i >= 0; i--) { - out[8 - i] = HEX[(argb >>> (i * 4)) & 0xF]; - } - return new String(out); - } - - public String id() { - return id; - } - - public String getId() { - return id; - } - - public boolean visible() { - return visible; - } - - public float rootX() { - return rootX; - } - - public float rootY() { - return rootY; - } - - public float width() { - return width; - } - - public float height() { - return height; - } - - public float radius() { - return radius; - } - - public float scale() { - return scale; - } - - public CompactHudStatModel setVisible(boolean visible) { - this.visible = visible; - return this; - } - - public CompactHudStatModel setRoot(float x, float y, float width, float height, float radius, float scale) { - this.rootX = x; - this.rootY = y; - this.width = width; - this.height = height; - this.radius = radius; - this.scale = scale; - return this; - } - - public Background background() { - return background; - } - - public String backgroundEffect() { - return background.effect; - } - - public float blurAlpha() { - return background.blurAlpha; - } - - public Icon icon() { - return icon; - } - - public Divider divider() { - return divider; - } - - public TextRun value() { - return value; - } - - public TextRun unit() { - return unit; - } - - public TextRun extra() { - return extra; - } - - public Animation animation() { - return animation; - } - - public CompactHudStatModel data(String key, Object value) { - if (key != null && !key.isEmpty()) { - data.put(key, value); - } - return this; - } - - public LinkedHashMap toProps() { - LinkedHashMap props = new LinkedHashMap<>(72); - props.put("id", id); - props.put("visible", visible); - props.put("rootX", rootX); - props.put("rootY", rootY); - props.put("width", width); - props.put("height", height); - props.put("radius", radius); - props.put("scale", scale); - background.writeTo(props); - icon.writeTo(props); - divider.writeTo(props); - value.writeTo(props, "value"); - unit.writeTo(props, "unit"); - extra.writeTo(props, "extra"); - animation.writeTo(props); - props.putAll(data); - return props; - } - - public LinkedHashMap toTemplateProps() { - LinkedHashMap props = toProps(); - float reservedRootWidth = reservedRootWidth(); - float reservedValueWidth = reservedValueWidth(); - float reservedUnitWidth = reservedUnitWidth(); - float reservedExtraWidth = reservedExtraWidth(); - props.put("width", width); - props.put("valueW", reservedValueWidth); - props.put("unitW", reservedUnitWidth); - props.put("extraW", reservedExtraWidth); - props.put("reservedValueWidth", reservedValueWidth); - props.put("reservedUnitWidth", reservedUnitWidth); - props.put("reservedExtraWidth", reservedExtraWidth); - props.put("valueText", value.text); - props.put("previousValue", animation.previousValue); - props.put("digitProgress", animation.digitProgress); - props.put("digitOffset", animation.digitOffset); - props.put("effectTime", 0.0f); - props.put("xText", stringData("xText", "0")); - props.put("yText", stringData("yText", "0")); - props.put("zText", stringData("zText", "0")); - props.put("netherText", stringData("netherText", "")); - props.remove("fps"); - props.remove("ping"); - props.remove("tps"); - props.remove("usedMiB"); - props.remove("maxMiB"); - props.remove("percent"); - props.remove("bps"); - props.remove("displayedBps"); - props.remove("x"); - props.remove("y"); - props.remove("z"); - props.remove("netherX"); - props.remove("netherZ"); - return props; - } - - /** - * Structure/template signature. Excludes volatile text/data values so HUD counters - * do not force JS execution and reconciliation every frame. - */ - public long structuralSignature() { - long h = 0xcbf29ce484222325L; - h = mix(h, id); - h = mix(h, visible); - h = mix(h, scale); - h = background.mixStructure(h); - h = icon.mixStructure(h); - h = divider.mixStructure(h); - h = value.mixStructure(h); - h = unit.mixStructure(h); - h = extra.mixStructure(h); - h = animation.mixStructure(h); - h = mixDataShape(h); - return h; - } - - /** - * Dynamic signature for cheap runtime prop patching. - */ - public long dynamicSignature() { - long h = 0xcbf29ce484222325L; - h = mix(h, icon.color); - h = mix(h, divider.color); - h = value.mixDynamic(h); - h = unit.mixDynamic(h); - h = extra.mixDynamic(h); - h = animation.mixDynamic(h); - for (var entry : data.entrySet()) { - h = mix(h, entry.getKey()); - h = mixObject(h, entry.getValue()); - } - return h; - } - - public long treeSignature() { - return structuralSignature(); - } - - public long layoutSignature(long structuralSignature) { - long h = structuralSignature; - h = mix(h, rootX); - h = mix(h, rootY); - h = mix(h, reservedRootWidth()); - h = mix(h, height); - h = mix(h, radius); - h = background.mixLayout(h); - h = icon.mixLayout(h); - h = divider.mixLayout(h); - h = value.mixLayout(h, reservedValueWidth()); - h = unit.mixLayout(h, reservedUnitWidth()); - h = extra.mixLayout(h, reservedExtraWidth()); - return h; - } - - public long layoutSignature(long structuralSignature, long ignoredDynamicSignature) { - return layoutSignature(structuralSignature); - } - - public long layoutSignature() { - return layoutSignature(structuralSignature()); - } - - public LinkedHashMap> runtimePatches() { - LinkedHashMap> patches = new LinkedHashMap<>(12); - putPropPatch(patches, "compact-stat:" + id, "renderBlurAlpha", background.blurAlpha); - putPropPatch(patches, "fill", "startColor", color(background.primary)); - putPropPatch(patches, "fill", "endColor", color(background.secondary)); - float paintAlpha = Math.max(alpha01(background.primary), alpha01(background.secondary)); - float strokeAlpha = Math.max(alpha01(background.stroke), paintAlpha); - putPropPatch(patches, "fill", "stroke", alpha(color(background.stroke), strokeAlpha)); - putPropPatch(patches, "fill", "strokeStartColor", alpha("#FFFFFFFF", 0.16f * strokeAlpha)); - putPropPatch(patches, "fill", "strokeEndColor", alpha(color(background.stroke), strokeAlpha)); - putPropPatch(patches, "top-glint", "startColor", alpha("#FFFFFFFF", 0.065f * paintAlpha)); - putPropPatch(patches, "top-glint", "endColor", "#00000000"); - putPropPatch(patches, "icon:texture", "tint", color(icon.color)); - putPropPatch(patches, "icon:glyph", "color", color(icon.color)); - putPropPatch(patches, "divider", "fill", color(divider.color)); - putTextPatch(patches, "value", value.text, color(value.color)); - putTextPatch(patches, "unit", unit.text, color(unit.color)); - putTextPatch(patches, "extra", extra.text, color(extra.color)); - float progress = clamp01(animation.digitProgress); - float offset = animation.digitOffset; - putTextPatch(patches, "digit:current", value.text, alpha(color(value.color), progress), -offset * (1.0f - progress) + 2.0f); - putTextPatch(patches, "digit:prev", animation.previousValue, alpha(color(value.color), 1.0f - progress), offset * progress + 2.0f); - putTextPatch(patches, "x:v", stringData("xText", "0"), stringData("valueColor", color(value.color))); - putTextPatch(patches, "y:v", stringData("yText", "0"), stringData("valueColor", color(value.color))); - putTextPatch(patches, "z:v", stringData("zText", "0"), stringData("valueColor", color(value.color))); - putTextPatch(patches, "nether", stringData("netherText", ""), color(extra.color)); - return patches; - } - - public LinkedHashMap runtimeBoundsPatches() { - LinkedHashMap patches = new LinkedHashMap<>(24); - float visualW = visualWidth(); - putBounds(patches, "compact-stat:" + id, rootX, rootY, visualW, height); - putBounds(patches, "clip", rootX, rootY, visualW, height); - putBounds(patches, "fill", rootX, rootY, visualW, height); - putBounds(patches, "top-glint", rootX + 1.0f, rootY + 1.0f, Math.max(0.0f, visualW - 2.0f), Math.max(1.0f, height * 0.42f)); - if (icon.visible) { - putBounds(patches, "icon:texture", rootX + icon.x, rootY + icon.y, icon.width, icon.height); - putBounds(patches, "icon:glyph", rootX + icon.x, rootY + icon.y, icon.width, icon.height); - } - if (divider.visible) { - putBounds(patches, "divider", rootX + divider.x, rootY + divider.y, divider.width, divider.height); - } - - if ("xyz".equals(id)) { - putCoordinateBounds(patches, visualW); - } else if (animation.digitAnimation) { - putBounds(patches, "digit:layer", rootX, rootY, width, height); - putBounds(patches, "digit:clip", rootX + value.x, rootY + value.y - 2.0f, value.width, Math.max(8.0f, height)); - putBounds(patches, "digit:prev", rootX + value.x, rootY + value.y - 2.0f, value.width, Math.max(8.0f, height)); - putBounds(patches, "digit:current", rootX + value.x, rootY + value.y - 2.0f, value.width, Math.max(8.0f, height)); - if (unit.visible) - putBounds(patches, "unit", rootX + unit.x, rootY + unit.y, unit.width, Math.max(8.0f, height)); - else putBounds(patches, "unit", rootX, rootY, 0.0f, 0.0f); - if (extra.visible) - putBounds(patches, "extra", rootX + extra.x, rootY + extra.y, extra.width, Math.max(8.0f, height)); - else putBounds(patches, "extra", rootX, rootY, 0.0f, 0.0f); - } else { - putBounds(patches, "text:layer", rootX, rootY, width, height); - if (value.visible) - putBounds(patches, "value", rootX + value.x, rootY + value.y, value.width, Math.max(8.0f, height)); - else putBounds(patches, "value", rootX, rootY, 0.0f, 0.0f); - if (unit.visible) - putBounds(patches, "unit", rootX + unit.x, rootY + unit.y, unit.width, Math.max(8.0f, height)); - else putBounds(patches, "unit", rootX, rootY, 0.0f, 0.0f); - if (extra.visible) - putBounds(patches, "extra", rootX + extra.x, rootY + extra.y, extra.width, Math.max(8.0f, height)); - else putBounds(patches, "extra", rootX, rootY, 0.0f, 0.0f); - } - return patches; - } - - private long mixDataShape(long h) { - for (var entry : data.entrySet()) { - String key = entry.getKey(); - Object value = entry.getValue(); - h = mix(h, key); - if (value instanceof Boolean b) { - h = mix(h, b); - } else if ("labelColor".equals(key) || "valueColor".equals(key)) { - h = mixObject(h, value); - } else if ("netherText".equals(key)) { - h = mix(h, value != null && !value.toString().isEmpty()); - } else if (value != null) { - h = mix(h, value.getClass().getName()); - } else { - h = mix(h, 0); - } - } - return h; - } - - private float reservedRootWidth() { - float s = scale != 0.0f ? scale : 1.0f; - return switch (id) { - case "fps" -> (26.0f + 36.0f + 2.0f + 17.0f + 5.0f) * s; - case "ping" -> (26.0f + 40.0f + 2.0f + 16.0f + 5.0f) * s; - case "tps" -> (26.0f + 39.0f + 2.0f + 15.0f + 3.5f) * s; - case "memory" -> (26.0f + 74.0f + 2.0f + 17.0f + 5.0f + 34.0f + 5.0f) * s; - case "speed_bps" -> (26.0f + 36.0f + 2.0f + 20.0f + 3.5f) * s; - case "game_time" -> (28.5f + 45.0f + 2.0f + 18.0f + 5.0f) * s; - case "system_time" -> (28.5f + 66.0f + 2.0f + 18.0f + 5.0f) * s; - case "xyz" -> (booleanData("showNether") ? 150.0f : 112.0f) * s; - default -> Math.max(width, (26.0f + 42.0f + 2.0f + 18.0f + 5.0f) * s); - }; - } - - private float reservedValueWidth() { - float s = scale != 0.0f ? scale : 1.0f; - float slot = switch (id) { - case "fps" -> 36.0f; - case "ping" -> 40.0f; - case "tps" -> 39.0f; - case "memory" -> 74.0f; - case "speed_bps" -> 36.0f; - case "game_time" -> 45.0f; - case "system_time" -> 66.0f; - case "xyz" -> 28.0f; - default -> 42.0f; - }; - return Math.max(value.width, slot * s); - } - - private float reservedUnitWidth() { - float s = scale != 0.0f ? scale : 1.0f; - float slot = switch (id) { - case "fps", "memory" -> 17.0f; - case "ping" -> 16.0f; - case "tps" -> 15.0f; - case "speed_bps" -> 20.0f; - default -> 18.0f; - }; - return Math.max(unit.width, slot * s); - } - - private float reservedExtraWidth() { - float s = scale != 0.0f ? scale : 1.0f; - float slot = switch (id) { - case "memory" -> 34.0f; - case "xyz" -> booleanData("showNether") ? 34.0f : 0.0f; - default -> 0.0f; - }; - return Math.max(extra.width, slot * s); - } - - private boolean booleanData(String key) { - Object value = data.get(key); - return value instanceof Boolean b && b; - } - - private void putCoordinateBounds(LinkedHashMap patches, float visualW) { - float rowX = rootX + value.x; - float rowY = rootY + value.y; - float rowH = Math.max(8.0f, height - value.y); - float pairGap = Math.max(1.0f, scale * 1.6f); - float gap = Math.max(2.0f, scale * 3.2f); - float labelXW = numberData("labelXW", 6.0f * scale); - float labelYW = numberData("labelYW", labelXW); - float labelZW = numberData("labelZW", labelXW); - float valueXW = numberData("valueXW", 28.0f * scale); - float valueYW = numberData("valueYW", valueXW); - float valueZW = numberData("valueZW", valueXW); - float extraW = numberData("xyzExtraW", extra.width); - float cursor = rowX; - putBounds(patches, "coords:row", rowX, rowY, Math.max(0.0f, visualW - value.x - 4.0f), rowH); - putBounds(patches, "x:l", cursor, rowY, labelXW, rowH); - cursor += labelXW + pairGap; - putBounds(patches, "x:v", cursor, rowY, valueXW, rowH); - cursor += valueXW + gap; - putBounds(patches, "y:l", cursor, rowY, labelYW, rowH); - cursor += labelYW + pairGap; - putBounds(patches, "y:v", cursor, rowY, valueYW, rowH); - cursor += valueYW + gap; - putBounds(patches, "z:l", cursor, rowY, labelZW, rowH); - cursor += labelZW + pairGap; - putBounds(patches, "z:v", cursor, rowY, valueZW, rowH); - cursor += valueZW + gap; - if (booleanData("showNether")) { - putBounds(patches, "nether", cursor, rowY, extraW, rowH); - } - } - - private float visualWidth() { - if ("xyz".equals(id) && booleanData("showNether")) { - return Math.max(1.0f, width - 4.0f * scale); - } - return width; - } - - private float numberData(String key, float fallback) { - Object value = data.get(key); - if (value instanceof Number n) return n.floatValue(); - if (value instanceof String s) { - try { - return Float.parseFloat(s); - } catch (NumberFormatException ignored) { - } - } - return fallback; - } - - private String stringData(String key, String fallback) { - Object value = data.get(key); - return value != null ? String.valueOf(value) : fallback; - } - - public static final class Background { - private String effect = "None"; - private boolean theme; - private float blurAlpha; - private int primary; - private int secondary; - private int stroke; - private float strokeWidth; - private float softness; - - public Background set(String effect, - boolean theme, - float blurAlpha, - int primary, - int secondary, - int stroke, - float strokeWidth, - float softness) { - this.effect = effect != null ? effect : "None"; - this.theme = theme; - this.blurAlpha = blurAlpha; - this.primary = primary; - this.secondary = secondary; - this.stroke = stroke; - this.strokeWidth = strokeWidth; - this.softness = softness; - return this; - } - - private void writeTo(LinkedHashMap props) { - props.put("backgroundEffect", effect); - props.put("backgroundTheme", theme); - props.put("blurAlpha", blurAlpha); - props.put("bgPrimary", color(primary)); - props.put("bgSecondary", color(secondary)); - props.put("stroke", color(stroke)); - props.put("strokeWidth", strokeWidth); - props.put("softness", softness); - } - - private long mix(long h) { - h = CompactHudStatModel.mix(h, effect); - h = CompactHudStatModel.mix(h, theme); - h = CompactHudStatModel.mix(h, blurAlpha); - h = CompactHudStatModel.mix(h, primary); - h = CompactHudStatModel.mix(h, secondary); - h = CompactHudStatModel.mix(h, stroke); - h = CompactHudStatModel.mix(h, strokeWidth); - h = CompactHudStatModel.mix(h, softness); - return h; - } - - private long mixStructure(long h) { - h = CompactHudStatModel.mix(h, effect); - return h; - } - - private long mixLayout(long h) { - h = CompactHudStatModel.mix(h, strokeWidth); - h = CompactHudStatModel.mix(h, softness); - return h; - } - } - - public static final class Icon { - private boolean visible; - private String kind = ""; - private String id = ""; - private String glyph = ""; - private String font = ""; - private float x; - private float y; - private float width; - private float height; - private float scale; - private int color; - - public Icon texture(String id, float x, float y, float width, float height, int color) { - return set(true, "texture", id, "", "", x, y, width, height, 1.0f, color); - } - - public Icon glyph(String glyph, String font, float x, float y, float width, float height, float scale, int color) { - return set(true, "glyph", "", glyph, font, x, y, width, height, scale, color); - } - - public Icon hidden() { - return set(false, "", "", "", "", 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0); - } - - private Icon set(boolean visible, - String kind, - String id, - String glyph, - String font, - float x, - float y, - float width, - float height, - float scale, - int color) { - this.visible = visible; - this.kind = kind != null ? kind : ""; - this.id = id != null ? id : ""; - this.glyph = glyph != null ? glyph : ""; - this.font = font != null ? font : ""; - this.x = x; - this.y = y; - this.width = width; - this.height = height; - this.scale = scale; - this.color = color; - return this; - } - - private void writeTo(LinkedHashMap props) { - props.put("iconVisible", visible); - props.put("iconKind", kind); - props.put("iconId", id); - props.put("iconGlyph", glyph); - props.put("iconFont", font); - props.put("iconX", x); - props.put("iconY", y); - props.put("iconW", width); - props.put("iconH", height); - props.put("iconScale", scale); - props.put("iconColor", color(color)); - } - - private long mix(long h) { - h = CompactHudStatModel.mix(h, visible); - h = CompactHudStatModel.mix(h, kind); - h = CompactHudStatModel.mix(h, id); - h = CompactHudStatModel.mix(h, glyph); - h = CompactHudStatModel.mix(h, font); - h = CompactHudStatModel.mix(h, x); - h = CompactHudStatModel.mix(h, y); - h = CompactHudStatModel.mix(h, width); - h = CompactHudStatModel.mix(h, height); - h = CompactHudStatModel.mix(h, scale); - return h; - } - - private long mixStructure(long h) { - h = CompactHudStatModel.mix(h, visible); - h = CompactHudStatModel.mix(h, kind); - h = CompactHudStatModel.mix(h, id); - h = CompactHudStatModel.mix(h, glyph); - h = CompactHudStatModel.mix(h, font); - return h; - } - - private long mixLayout(long h) { - h = CompactHudStatModel.mix(h, x); - h = CompactHudStatModel.mix(h, y); - h = CompactHudStatModel.mix(h, width); - h = CompactHudStatModel.mix(h, height); - h = CompactHudStatModel.mix(h, scale); - return h; - } - } - - public static final class Divider { - private boolean visible; - private float x; - private float y; - private float width; - private float height; - private int color; - - public Divider set(float x, float y, float width, float height, int color) { - this.visible = true; - this.x = x; - this.y = y; - this.width = width; - this.height = height; - this.color = color; - return this; - } - - private void writeTo(LinkedHashMap props) { - props.put("dividerVisible", visible); - props.put("dividerX", x); - props.put("dividerY", y); - props.put("dividerW", width); - props.put("dividerH", height); - props.put("dividerColor", color(color)); - } - - private long mix(long h) { - h = CompactHudStatModel.mix(h, visible); - h = CompactHudStatModel.mix(h, x); - h = CompactHudStatModel.mix(h, y); - h = CompactHudStatModel.mix(h, width); - h = CompactHudStatModel.mix(h, height); - return h; - } - - private long mixStructure(long h) { - h = CompactHudStatModel.mix(h, visible); - return h; - } - - private long mixLayout(long h) { - h = CompactHudStatModel.mix(h, x); - h = CompactHudStatModel.mix(h, y); - h = CompactHudStatModel.mix(h, width); - h = CompactHudStatModel.mix(h, height); - return h; - } - } - - public static final class TextRun { - private boolean visible; - private String text = ""; - private String font = ""; - private float scale; - private float x; - private float y; - private float width; - private int color; - - public TextRun set(String text, String font, float scale, float x, float y, float width, int color) { - this.visible = text != null && !text.isEmpty(); - this.text = text != null ? text : ""; - this.font = font != null ? font : ""; - this.scale = scale; - this.x = x; - this.y = y; - this.width = width; - this.color = color; - return this; - } - - public TextRun hidden() { - this.visible = false; - this.text = ""; - this.font = ""; - this.scale = 1.0f; - this.x = 0.0f; - this.y = 0.0f; - this.width = 0.0f; - this.color = 0; - return this; - } - - private void writeTo(LinkedHashMap props, String prefix) { - props.put(prefix + "Visible", visible); - props.put(prefix + "Text", text); - props.put(prefix + "Font", font); - props.put(prefix + "Scale", scale); - props.put(prefix + "X", x); - props.put(prefix + "Y", y); - props.put(prefix + "W", width); - props.put(prefix + "Color", color(color)); - } - - private long mixStructure(long h) { - h = CompactHudStatModel.mix(h, visible); - h = CompactHudStatModel.mix(h, font); - h = CompactHudStatModel.mix(h, scale); - return h; - } - - private long mixLayout(long h, float reservedWidth) { - h = CompactHudStatModel.mix(h, reservedWidth); - return h; - } - - private long mixDynamic(long h) { - h = CompactHudStatModel.mix(h, visible); - h = CompactHudStatModel.mix(h, text); - h = CompactHudStatModel.mix(h, color); - return h; - } - } - - public static final class Animation { - private String labelEffect = "NONE"; - private int labelEffectSpeed; - private float effectTime; - private boolean digitAnimation; - private String previousValue = ""; - private float digitProgress = 1.0f; - private float digitOffset; - - public Animation setLabelEffect(String effect, int speed, float time) { - this.labelEffect = effect != null ? effect : "NONE"; - this.labelEffectSpeed = speed; - this.effectTime = time; - return this; - } - - public Animation setDigitAnimation(boolean enabled, String previousValue, float progress, float offset) { - this.digitAnimation = enabled; - this.previousValue = previousValue != null ? previousValue : ""; - this.digitProgress = progress; - this.digitOffset = offset; - return this; - } - - private void writeTo(LinkedHashMap props) { - props.put("labelEffect", labelEffect); - props.put("labelEffectSpeed", labelEffectSpeed); - props.put("effectTime", effectTime); - props.put("digitAnimation", digitAnimation); - props.put("previousValue", previousValue); - props.put("digitProgress", digitProgress); - props.put("digitOffset", digitOffset); - } - - private long mixStructure(long h) { - h = CompactHudStatModel.mix(h, labelEffect); - h = CompactHudStatModel.mix(h, labelEffectSpeed); - h = CompactHudStatModel.mix(h, digitAnimation); - return h; - } - - private long mixDynamic(long h) { - h = CompactHudStatModel.mix(h, effectTime); - h = CompactHudStatModel.mix(h, previousValue); - h = CompactHudStatModel.mix(h, digitProgress); - h = CompactHudStatModel.mix(h, digitOffset); - return h; - } - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/core/Module.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/core/Module.java deleted file mode 100644 index 1efe8ba..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/core/Module.java +++ /dev/null @@ -1,1031 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.features.module.core; - -import com.mojang.blaze3d.vertex.PoseStack; -import dev.loki.lovisual.config.common.CommonBooleanGroupSchema; -import dev.loki.lovisual.config.common.CommonSettingSchema; -import dev.loki.lovisual.config.definition.ConfigNameProvider; -import dev.loki.lovisual.config.definition.ConfigObject; -import dev.loki.lovisual.config.definition.SettingDef; -import dev.loki.lovisual.config.definition.SettingOwner; -import dev.loki.lovisual.config.i18n.ConfigValueAliasProvider; -import dev.loki.lovisual.config.io.ConfigSerializer; -import dev.loki.lovisual.config.values.collection.ConfigValue; -import dev.loki.lovisual.config.values.collection.ItemIdSetValue; -import dev.loki.lovisual.config.values.collection.SetValue; -import dev.loki.lovisual.config.values.color.KeyBindValue; -import dev.loki.lovisual.config.values.color.RGBAColorValue; -import dev.loki.lovisual.config.values.color.RGBColorValue; -import dev.loki.lovisual.config.values.mode.BindMode; -import dev.loki.lovisual.config.values.mode.EnumValue; -import dev.loki.lovisual.config.values.mode.ModeValue; -import dev.loki.lovisual.config.values.primitive.BooleanMapValue; -import dev.loki.lovisual.config.values.primitive.BooleanValue; -import dev.loki.lovisual.config.values.primitive.NumberValue; -import dev.loki.lovisual.config.values.primitive.StringValue; -import dev.loki.lovisual.events.impl.render.RenderPrewarmCollectEvent; -import dev.loki.lovisual.config.values.collection.ConfigValue; -import dev.loki.lovisual.config.values.collection.ItemIdSetValue; -import dev.loki.lovisual.config.values.collection.SetValue; -import dev.loki.lovisual.config.values.color.KeyBindValue; -import dev.loki.lovisual.config.values.color.RGBAColorValue; -import dev.loki.lovisual.config.values.color.RGBColorValue; -import dev.loki.lovisual.config.values.mode.BindMode; -import dev.loki.lovisual.config.values.mode.EnumValue; -import dev.loki.lovisual.config.values.mode.ModeValue; -import dev.loki.lovisual.config.values.primitive.BooleanMapValue; -import dev.loki.lovisual.config.values.primitive.BooleanValue; -import dev.loki.lovisual.config.values.primitive.NumberValue; -import dev.loki.lovisual.config.values.primitive.StringValue; -import dev.loki.lovisual.addon.manager.ModuleExtensionManager; -import dev.loki.lovisual.features.gui.clickgui.settings.bind.FunctionBindSetting; -import dev.loki.lovisual.features.gui.clickgui.settings.bind.KeyBindSetting; -import dev.loki.lovisual.features.gui.clickgui.settings.core.Setting; -import dev.loki.lovisual.features.gui.clickgui.settings.core.SettingFactory; -import dev.loki.lovisual.features.gui.clickgui.settings.text.list.TextListSetting; - -import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.client.renderer.SubmitNodeCollector; -import dev.loki.lovisual.config.common.CommonBooleanGroupSchema; -import dev.loki.lovisual.config.common.CommonSettingSchema; - -import dev.loki.lovisual.features.module.module.ModuleBinds; -import dev.loki.lovisual.features.module.module.ModuleConfigValues; -import dev.loki.lovisual.features.module.module.ModuleNames; -import dev.loki.lovisual.features.module.module.ModuleSettingValues; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.renderer.Renderer3D; -import dev.loki.lovisual.render.engine.text.font.info.FontInfo; -import dev.loki.lovisual.render.engine.text.render.TextRenderer; -import dev.loki.lovisual.Lifecycle; -import dev.loki.lovisual.util.logging.DebugLog; -import java.util.*; -import java.util.function.Supplier; -import dev.loki.lovisual.features.module.core.ModuleCategory; -import dev.loki.lovisual.features.module.core.ModuleInfo; -import dev.loki.lovisual.features.module.core.ModuleManager; -import dev.loki.lovisual.features.module.lifecycle.ModuleActivationSource; -import dev.loki.lovisual.features.module.lifecycle.ModuleAutoLoader; -import dev.loki.lovisual.features.module.phase.HudPhase; -import dev.loki.lovisual.features.module.phase.WorldPhase; - -public abstract class Module implements ConfigObject, ConfigNameProvider, SettingOwner, ConfigValueAliasProvider { - - protected final List settings = new ArrayList<>(); - private final String name; - private final String displayName; - private final List aliases; - private final String description; - private final ModuleCategory category; - private final BooleanValue enabledValue; - private final EnumValue activationSourceValue = - new EnumValue<>("activation_source", ModuleActivationSource.NONE, ModuleActivationSource.class); - private final BooleanValue moduleListVisibleValue = new BooleanValue(ModuleConfigValues.SETTING_SHOW_IN_MODULE_LIST, true); - private final List declaredSettingDefs = new ArrayList<>(); - private final Map, SettingDef> declaredSettingDefsByValue = new IdentityHashMap<>(); - private final KeyBindSetting keyBindSetting; - private final Map actions = new LinkedHashMap<>(); - private boolean enabled; - private ModuleActivationSource activationSource = ModuleActivationSource.NONE; - - protected Module() { - ModuleInfo info = getClass().getAnnotation(ModuleInfo.class); - if (info == null) { - throw new IllegalStateException(getClass().getName() + " must be annotated with @ModuleInfo"); - } - - this.name = info.id().toLowerCase(Locale.ROOT); - this.displayName = info.displayName(); - this.aliases = ModuleNames.normalizeAliases(info.aliases()); - this.description = info.description(); - this.category = info.category(); - this.enabledValue = new BooleanValue("enabled", info.enabledByDefault()); - this.keyBindSetting = createKeyBindSetting(); - - settings.add(keyBindSetting); - } - - @Deprecated(forRemoval = true) - public Module(String name, String displayName, ModuleCategory category) { - this(name, displayName, category, ""); - } - - @Deprecated(forRemoval = true) - public Module(String name, String displayName, ModuleCategory category, String description) { - this.name = name.toLowerCase(Locale.ROOT); - this.displayName = displayName; - this.aliases = List.of(); - this.description = description == null ? "" : description; - this.category = category; - this.enabledValue = new BooleanValue("enabled", false); - this.keyBindSetting = createKeyBindSetting(); - - settings.add(keyBindSetting); - } - - private static KeyBindSetting createKeyBindSetting() { - return ModuleBinds.createKeyBindSetting(); - } - - private static ModuleActivationSource normalizeActivationSource(boolean state, ModuleActivationSource source) { - if (!state) return ModuleActivationSource.NONE; - return source == null || source == ModuleActivationSource.NONE ? ModuleActivationSource.INTERNAL : source; - } - - /** - * Must be called after subclass construction completes so that its fields are initialized - * before settings are added. ModuleAutoLoader calls this automatically. - */ - public final void postInit() { - SettingDef.applyI18nAnnotations(this, declaredSettingDefs); - settings.addAll(SettingFactory.fromDefs(declaredSettingDefs)); - loadSettings(settings); - - // attach parent for settings so they can request saves - for (Setting s : settings) { - s.setParent(this); - s.preflightI18n(); - } - - ModuleManager.register(this); - } - - @Deprecated(forRemoval = false) - protected void loadSettings(List settings) { - } - - protected final BooleanValue bool(String configName, boolean def) { - return bool(configName, settingIdFromConfigName(configName), def); - } - - protected final BooleanValue bool(String configName, String settingId, boolean def) { - return ModuleSettingValues.bool(configName, settingId, def, this::declare); - } - - protected final NumberValue num(String configName, int def, int min, int max) { - return num(configName, settingIdFromConfigName(configName), def, min, max); - } - - protected final NumberValue num(String configName, String settingId, int def, int min, int max) { - return ModuleSettingValues.num(configName, settingId, def, min, max, this::declare); - } - - protected final NumberValue num(String configName, long def, long min, long max) { - return num(configName, settingIdFromConfigName(configName), def, min, max); - } - - protected final NumberValue num(String configName, String settingId, long def, long min, long max) { - return ModuleSettingValues.num(configName, settingId, def, min, max, this::declare); - } - - protected final NumberValue num(String configName, float def, float min, float max) { - return num(configName, settingIdFromConfigName(configName), def, min, max); - } - - protected final NumberValue num(String configName, String settingId, float def, float min, float max) { - return ModuleSettingValues.num(configName, settingId, def, min, max, this::declare); - } - - protected final NumberValue num(String configName, double def, double min, double max) { - return num(configName, settingIdFromConfigName(configName), def, min, max); - } - - protected final NumberValue num(String configName, String settingId, double def, double min, double max) { - return ModuleSettingValues.num(configName, settingId, def, min, max, this::declare); - } - - protected final ModeValue mode(String configName, String def, String... options) { - return modeSetting(configName, settingIdFromConfigName(configName), def, options); - } - - protected final ModeValue modeSetting(String configName, String settingId, String def, String... options) { - return ModuleSettingValues.modeSetting(configName, settingId, def, this::declare, options); - } - - protected final > EnumValue enumMode(String configName, E def) { - return enumSetting(configName, settingIdFromConfigName(configName), def); - } - - protected final > EnumValue enumSetting(String configName, String settingId, E def) { - return ModuleSettingValues.enumSetting(configName, settingId, def, this::declare); - } - - @Deprecated(forRemoval = false) - protected final > EnumValue enumMode(String configName, E def, Class enumClass) { - return enumSetting(configName, settingIdFromConfigName(configName), def, enumClass); - } - - @Deprecated(forRemoval = false) - protected final > EnumValue enumSetting(String configName, String settingId, E def, Class enumClass) { - return ModuleSettingValues.enumSetting(configName, settingId, def, enumClass, this::declare); - } - - @SafeVarargs - protected final > EnumValue enumMode(String configName, E def, E... options) { - return enumSetting(configName, settingIdFromConfigName(configName), def, options); - } - - @SafeVarargs - protected final > EnumValue enumSetting(String configName, String settingId, E def, E... options) { - return ModuleSettingValues.enumSetting(configName, settingId, def, this::declare, options); - } - - protected final RGBAColorValue color(String configName, String defHex) { - return color(configName, settingIdFromConfigName(configName), defHex); - } - - protected final RGBAColorValue color(String configName, String settingId, String defHex) { - return ModuleSettingValues.color(configName, settingId, defHex, this::declare); - } - - protected final RGBColorValue colorNoAlpha(String configName, String defHex) { - return colorNoAlpha(configName, settingIdFromConfigName(configName), defHex); - } - - protected final RGBColorValue colorNoAlpha(String configName, String settingId, String defHex) { - return ModuleSettingValues.colorNoAlpha(configName, settingId, defHex, this::declare); - } - - protected final StringValue text(String configName, String def) { - return text(configName, settingIdFromConfigName(configName), def); - } - - protected final StringValue text(String configName, String settingId, String def) { - return ModuleSettingValues.text(configName, settingId, def, this::declare); - } - - protected final SetValue textList(String configName) { - return textList(configName, settingIdFromConfigName(configName), TextListSetting.PickerMode.ALL); - } - - protected final SetValue textList(String configName, TextListSetting.PickerMode pickerMode) { - return textList(configName, settingIdFromConfigName(configName), pickerMode); - } - - protected final SetValue textList(String configName, String settingId, TextListSetting.PickerMode pickerMode) { - return ModuleSettingValues.textList(configName, settingId, pickerMode, this::declare); - } - - protected final SetValue textList(String configName, String settingId, TextListSetting.PickerMode pickerMode, Set defaults) { - return ModuleSettingValues.textList(configName, settingId, pickerMode, defaults, this::declare); - } - - protected final ItemIdSetValue itemList(String configName) { - return itemList(configName, settingIdFromConfigName(configName), TextListSetting.PickerMode.ITEMS); - } - - protected final ItemIdSetValue itemList(String configName, TextListSetting.PickerMode pickerMode) { - return itemList(configName, settingIdFromConfigName(configName), pickerMode); - } - - protected final ItemIdSetValue itemList(String configName, String settingId, TextListSetting.PickerMode pickerMode) { - return ModuleSettingValues.itemList(configName, settingId, pickerMode, this::declare); - } - - protected final BooleanMapValue group(String configName, Map defaults) { - return group(configName, settingIdFromConfigName(configName), defaults); - } - - protected final BooleanMapValue group(String configName, String settingId, Map defaults) { - return ModuleSettingValues.group(configName, settingId, defaults, this::declare); - } - - protected final KeyBindValue bind(String configName, String def, BindMode mode) { - return bind(configName, settingIdFromConfigName(configName), def, mode); - } - - protected final KeyBindValue bind(String configName, String settingId, String def, BindMode mode) { - return ModuleSettingValues.bind(configName, settingId, def, mode, this::declare); - } - - protected final BooleanMapValue groupCommon(String configName, - String commonI18nKey, - Map defaults) { - return groupCommon(configName, settingIdFromConfigName(configName), commonI18nKey, defaults); - } - - protected final BooleanMapValue groupCommon(String configName, - String settingId, - String commonI18nKey, - Map defaults) { - return common(group(configName, settingId, defaults), commonI18nKey); - } - - protected final ModeValue modeCommon(String configName, - String commonI18nKey, - String def, - String... options) { - return modeCommon(configName, settingIdFromConfigName(configName), commonI18nKey, def, options); - } - - protected final ModeValue modeCommon(String configName, - String settingId, - String commonI18nKey, - String def, - String... options) { - return common(modeSetting(configName, settingId, def, options), commonI18nKey); - } - - protected final > EnumValue enumCommon(String configName, - String commonI18nKey, - E def) { - return enumCommon(configName, settingIdFromConfigName(configName), commonI18nKey, def); - } - - protected final > EnumValue enumCommon(String configName, - String settingId, - String commonI18nKey, - E def) { - return common(enumSetting(configName, settingId, def), commonI18nKey); - } - - protected final > EnumValue enumCommon(String configName, - String commonI18nKey, - E def, - Class enumClass) { - return enumCommon(configName, settingIdFromConfigName(configName), commonI18nKey, def, enumClass); - } - - protected final > EnumValue enumCommon(String configName, - String settingId, - String commonI18nKey, - E def, - Class enumClass) { - return common(enumSetting(configName, settingId, def, enumClass), commonI18nKey); - } - - @SafeVarargs - protected final > EnumValue enumCommon(String configName, - String commonI18nKey, - E def, - E... options) { - return enumCommon(configName, settingIdFromConfigName(configName), commonI18nKey, def, options); - } - - @SafeVarargs - protected final > EnumValue enumCommon(String configName, - String settingId, - String commonI18nKey, - E def, - E... options) { - return common(enumSetting(configName, settingId, def, options), commonI18nKey); - } - - protected final NumberValue numCommon(String configName, - String commonI18nKey, - int def, - int min, - int max) { - return numCommon(configName, settingIdFromConfigName(configName), commonI18nKey, def, min, max); - } - - protected final NumberValue numCommon(String configName, - String settingId, - String commonI18nKey, - int def, - int min, - int max) { - return common(num(configName, settingId, def, min, max), commonI18nKey); - } - - protected final NumberValue numCommon(String configName, - String commonI18nKey, - float def, - float min, - float max) { - return numCommon(configName, settingIdFromConfigName(configName), commonI18nKey, def, min, max); - } - - protected final NumberValue numCommon(String configName, - String settingId, - String commonI18nKey, - float def, - float min, - float max) { - return common(num(configName, settingId, def, min, max), commonI18nKey); - } - - protected final NumberValue numCommon(String configName, - String commonI18nKey, - double def, - double min, - double max) { - return numCommon(configName, settingIdFromConfigName(configName), commonI18nKey, def, min, max); - } - - protected final NumberValue numCommon(String configName, - String settingId, - String commonI18nKey, - double def, - double min, - double max) { - return common(num(configName, settingId, def, min, max), commonI18nKey); - } - - protected final BooleanValue boolCommon(String configName, - String commonI18nKey, - boolean def) { - return boolCommon(configName, settingIdFromConfigName(configName), commonI18nKey, def); - } - - protected final BooleanValue boolCommon(String configName, - String settingId, - String commonI18nKey, - boolean def) { - return common(bool(configName, settingId, def), commonI18nKey); - } - - protected final BooleanMapValue groupCommon(String configName, - CommonBooleanGroupSchema schema) { - return groupCommon(configName, settingIdFromConfigName(configName), schema); - } - - protected final BooleanMapValue groupCommon(String configName, - String settingId, - CommonBooleanGroupSchema schema) { - if (schema == null) { - return group(configName, settingId, Map.of()); - } - return common(group(configName, settingId, schema.defaults()), schema); - } - - protected final BooleanMapValue protocolHeuristics(String configName, - String settingId, - CommonBooleanGroupSchema schema) { - return ModuleSettingValues.protocolHeuristics(configName, settingId, schema, this::declare); - } - - protected final > EnumValue enumCommon(String configName, - CommonSettingSchema schema, - E def) { - return enumCommon(configName, settingIdFromConfigName(configName), schema, def); - } - - protected final > EnumValue enumCommon(String configName, - String settingId, - CommonSettingSchema schema, - E def) { - return common(enumSetting(configName, settingId, def), schema); - } - - protected final > EnumValue enumCommon(String configName, - CommonSettingSchema schema, - E def, - Class enumClass) { - return enumCommon(configName, settingIdFromConfigName(configName), schema, def, enumClass); - } - - protected final > EnumValue enumCommon(String configName, - String settingId, - CommonSettingSchema schema, - E def, - Class enumClass) { - return common(enumSetting(configName, settingId, def, enumClass), schema); - } - - @SafeVarargs - protected final > EnumValue enumCommon(String configName, - CommonSettingSchema schema, - E def, - E... options) { - return enumCommon(configName, settingIdFromConfigName(configName), schema, def, options); - } - - @SafeVarargs - protected final > EnumValue enumCommon(String configName, - String settingId, - CommonSettingSchema schema, - E def, - E... options) { - return common(enumSetting(configName, settingId, def, options), schema); - } - - protected final ModeValue modeCommon(String configName, - CommonSettingSchema schema, - String def, - String... options) { - return modeCommon(configName, settingIdFromConfigName(configName), schema, def, options); - } - - protected final ModeValue modeCommon(String configName, - String settingId, - CommonSettingSchema schema, - String def, - String... options) { - return common(modeSetting(configName, settingId, def, options), schema); - } - - protected final NumberValue numCommon(String configName, - CommonSettingSchema schema, - int def, - int min, - int max) { - return numCommon(configName, settingIdFromConfigName(configName), schema, def, min, max); - } - - protected final NumberValue numCommon(String configName, - String settingId, - CommonSettingSchema schema, - int def, - int min, - int max) { - return common(num(configName, settingId, def, min, max), schema); - } - - protected final NumberValue numCommon(String configName, - CommonSettingSchema schema, - float def, - float min, - float max) { - return numCommon(configName, settingIdFromConfigName(configName), schema, def, min, max); - } - - protected final NumberValue numCommon(String configName, - String settingId, - CommonSettingSchema schema, - float def, - float min, - float max) { - return common(num(configName, settingId, def, min, max), schema); - } - - protected final NumberValue numCommon(String configName, - CommonSettingSchema schema, - double def, - double min, - double max) { - return numCommon(configName, settingIdFromConfigName(configName), schema, def, min, max); - } - - protected final NumberValue numCommon(String configName, - String settingId, - CommonSettingSchema schema, - double def, - double min, - double max) { - return common(num(configName, settingId, def, min, max), schema); - } - - protected final BooleanValue boolCommon(String configName, - CommonSettingSchema schema, - boolean def) { - return boolCommon(configName, settingIdFromConfigName(configName), schema, def); - } - - protected final BooleanValue boolCommon(String configName, - String settingId, - CommonSettingSchema schema, - boolean def) { - return common(bool(configName, settingId, def), schema); - } - - protected final > V visibleWhen(V value, Supplier supplier) { - SettingDef def = declaredSettingDefsByValue.get(value); - if (def != null) { - def.visibleWhen(supplier); - } - return value; - } - - protected final > V shownWhen(V value, Supplier supplier) { - return visibleWhen(value, supplier); - } - - protected final > V unavailableWhen(V value, Supplier supplier, String reason) { - SettingDef def = declaredSettingDefsByValue.get(value); - if (def != null) { - def.unavailableWhen(supplier, reason); - } - return value; - } - - protected final > V unavailableWhen(V value, Supplier supplier, Supplier reason) { - SettingDef def = declaredSettingDefsByValue.get(value); - if (def != null) { - def.unavailableWhen(supplier, reason); - } - return value; - } - - protected final > V notAppliedWhen(V value, Supplier supplier, String reason) { - return unavailableWhen(value, supplier, reason); - } - - protected final > V notAppliedWhen(V value, Supplier supplier, Supplier reason) { - return unavailableWhen(value, supplier, reason); - } - - protected final > V appliesWhen(V value, Supplier supplier, String reasonWhenUnavailable) { - SettingDef def = declaredSettingDefsByValue.get(value); - if (def != null) { - def.availableWhen(supplier, reasonWhenUnavailable); - } - return value; - } - - protected final > V appliesWhen(V value, Supplier supplier, Supplier reasonWhenUnavailable) { - SettingDef def = declaredSettingDefsByValue.get(value); - if (def != null) { - def.availableWhen(supplier, reasonWhenUnavailable); - } - return value; - } - - private void declare(ConfigValue value, SettingDef def) { - declaredSettingDefs.add(def); - declaredSettingDefsByValue.put(value, def); - } - - protected final SettingDef setting(SettingDef def) { - if (def != null && def.value() != null) { - declare(def.value(), def); - } - return def; - } - - public final void addExtensionSetting(SettingDef def) { - Setting setting = SettingFactory.fromDef(def); - if (setting == null) return; - setting.setParent(this); - setting.preflightI18n(); - settings.add(setting); - if (def != null && def.value() != null) { - declaredSettingDefsByValue.put(def.value(), def); - } - } - - protected final > V common(V value, String commonI18nKey) { - SettingDef def = declaredSettingDefsByValue.get(value); - if (def != null && commonI18nKey != null && !commonI18nKey.isBlank()) { - def.common(commonI18nKey); - } - return value; - } - - protected final > V common(V value, CommonSettingSchema schema) { - SettingDef def = declaredSettingDefsByValue.get(value); - if (def != null && schema != null) { - def.common(schema); - } - return value; - } - - private String settingIdFromConfigName(String configName) { - return ModuleNames.settingId(configName, name(), getClass().getSimpleName()); - } - - @Override - public List getLegacyValueNames(String currentValueName) { - if (ModuleConfigValues.SETTING_SHOW_IN_MODULE_LIST.equals(currentValueName)) { - return List.of(ModuleConfigValues.LEGACY_SETTING_SHOW_IN_ACTIVE_MODULES); - } - return List.of(); - } - - public List getSettings() { - return settings; - } - - public ConfigValue getConfigValue(String configName) { - if (configName == null || configName.isBlank()) return null; - for (ConfigValue value : getConfigValues()) { - if (value != null && configName.equals(value.getName())) { - return value; - } - } - return null; - } - - public KeyBindSetting getKeyBindSetting() { - return keyBindSetting; - } - - public Collection getActionSettings() { - return Collections.unmodifiableCollection(actions.values()); - } - - public void setDefaultBind(String keyName) { - keyBindSetting.setKeyByName(keyName); - } - - protected final FunctionBindSetting action(String name, String defaultKey, BindMode mode) { - return ModuleBinds.registerAction(actions, settings, name, defaultKey, mode); - } - - protected final FunctionBindSetting action(String name, String defaultKey) { - return action(name, defaultKey, BindMode.PRESS); - } - - protected final void addAction(String name, String defaultKey, BindMode mode) { - action(name, defaultKey, mode); - } - - protected final void addAction(String name, String defaultKey) { - action(name, defaultKey); - } - - protected final boolean isActionPressedOnce(String name) { - return ModuleBinds.isPressedOnce(isEnabled(), actions.get(name)); - } - - protected final boolean isActionHeld(String name) { - return ModuleBinds.isHeld(isEnabled(), actions.get(name)); - } - - protected final String getActionKey(String name) { - return ModuleBinds.actionKey(actions.get(name)); - } - - public boolean isShownInModuleList() { - return moduleListVisibleValue.get(); - } - - public void setShownInModuleList(boolean value) { - if (moduleListVisibleValue.get() == value) return; - moduleListVisibleValue.set(value); - ModuleConfigValues.requestSaveQuietly(this); - } - - public void toggleShownInModuleList() { - setShownInModuleList(!moduleListVisibleValue.get()); - } - - public boolean isEnabled() { - return enabled; - } - - protected String getUnavailableReason() { - return ""; - } - - public final boolean isAvailable() { - return getAvailabilityReason().isEmpty(); - } - - public final String getAvailabilityReason() { - try { - String reason = getUnavailableReason(); - return reason == null ? "" : reason.trim(); - } catch (Exception ignored) { - return ""; - } - } - - public final void setEnabled(boolean state) { - setEnabled(state, ModuleActivationSource.MANUAL); - } - - public ModuleActivationSource getActivationSource() { - return activationSource; - } - - public final void setActivationSource(ModuleActivationSource source) { - activationSource = normalizeActivationSource(enabled, source); - activationSourceValue.set(activationSource); - ModuleConfigValues.requestSaveQuietly(this); - } - - public boolean isEnabledFromKeybind() { - return enabled && activationSource == ModuleActivationSource.KEYBIND; - } - - public final void setEnabledFromKeybind(boolean state) { - setEnabled(state, ModuleActivationSource.KEYBIND); - } - - public String name() { - return name; - } - - @Override - public String getConfigName() { - return name(); - } - - public String getDisplayName() { - return ModuleNames.translateFirst(displayName, "module." + name(), "module." + name() + ".name"); - } - - public List getAliases() { - return aliases; - } - - public String getDescription() { - return ModuleNames.translateFirst(description, "module." + name() + ".description"); - } - - public ModuleCategory getCategory() { - return category; - } - - public final void toggle() { - setEnabled(!enabled, ModuleActivationSource.MANUAL); - } - - public final void toggleFromKeybind() { - setEnabled(!enabled, ModuleActivationSource.KEYBIND); - } - - public final void setEnabled(boolean state, ModuleActivationSource source) { - if (state && !Lifecycle.isActive()) { - return; - } - ModuleActivationSource normalizedSource = normalizeActivationSource(state, source); - if (enabled == state) { - if (activationSource == normalizedSource - && enabledValue.get() == enabled - && activationSourceValue.get() == activationSource) { - return; - } - activationSource = normalizedSource; - enabledValue.set(enabled); - activationSourceValue.set(activationSource); - ModuleConfigValues.requestSaveQuietly(this); - return; - } - - activationSource = normalizedSource; - enabled = state; - if (enabled) { - if (!ModuleExtensionManager.beforeEnable(this)) { - enabled = false; - activationSource = ModuleActivationSource.NONE; - return; - } - onEnable(); - ModuleExtensionManager.afterEnable(this); - } else { - if (!ModuleExtensionManager.beforeDisable(this)) { - enabled = true; - activationSource = normalizedSource; - return; - } - onDisable(); - ModuleExtensionManager.afterDisable(this); - } - ModuleManager.notifyListeners(name, enabled); - - // Persist change - enabledValue.set(enabled); - activationSourceValue.set(activationSource); - ModuleConfigValues.requestSaveQuietly(this); - } - - public final void setEnabledFromConfig(boolean state) { - setEnabled(state, ModuleActivationSource.CONFIG); - } - - public final void setEnabledInternal(boolean state) { - setEnabled(state, ModuleActivationSource.INTERNAL); - } - - public final void setEnabledManual(boolean state) { - setEnabled(state, ModuleActivationSource.MANUAL); - } - - final void setRuntimeEnabledTransient(boolean state) { - if (enabled == state) return; - enabled = state; - if (enabled) { - if (ModuleExtensionManager.beforeEnable(this)) { - onEnable(); - ModuleExtensionManager.afterEnable(this); - } else { - enabled = false; - } - } else if (ModuleExtensionManager.beforeDisable(this)) { - onDisable(); - ModuleExtensionManager.afterDisable(this); - } else { - enabled = true; - } - ModuleManager.notifyListeners(name, enabled); - } - - public void collectRenderPrewarm(RenderPrewarmCollectEvent event) { - if (event == null) return; - event.font("Onest", FontInfo.Type.Regular) - .font("OnestMedium", FontInfo.Type.Regular) - .font("OnestBold", FontInfo.Type.Regular) - .font("Inter", FontInfo.Type.Regular) - .font("Inter", FontInfo.Type.Bold) - .font("OnestMedium", FontInfo.Type.Regular) - .font("Iosevka", FontInfo.Type.Regular) - .font("Iosevka", FontInfo.Type.Bold); - } - - public void onTick() { - } - - public void onFrame(float tickDelta) { - } - - public void onKey(int key, int action) { - } - - public void onEnable() { - } - - public void onDisable() { - } - - public void onRender2D(GuiGraphicsExtractor ctx) { - } - - public void onRender2D(GuiGraphicsExtractor ctx, float tickDelta) { - onRender2D(ctx); - } - - public void onRenderHudEngine(Renderer2D renderer, TextRenderer textRenderer) { - } - - public void onRenderHudEngine(Renderer2D renderer, TextRenderer textRenderer, float tickDelta) { - onRenderHudEngine(renderer, textRenderer); - } - - public void onRenderHudEngine(Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta) { - onRenderHudEngine(renderer, textRenderer, tickDelta); - } - - public void onRenderHudEngineForeground(Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta) { - } - - public void onRenderWorld(PoseStack matrices, SubmitNodeCollector consumers) { - } - - public void onRenderWorld(PoseStack matrices, SubmitNodeCollector consumers, float tickDelta) { - onRenderWorld(matrices, consumers); - } - - public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer) { - } - - public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { - onRenderWorldEngine(renderer, depthRenderer); - } - - public HudPhase getHudPhase() { - return HudPhase.NONE; - } - - public WorldPhase getWorldPhase() { - return WorldPhase.NONE; - } - - @Override - public List> getConfigValues() { - return ModuleConfigValues.orderedValues(enabledValue, activationSourceValue, moduleListVisibleValue, settings); - } - - /** - * Legacy support: apply values from the old "settings" map saved by previous versions. - */ - public void applyLegacySettings(Object json) { - ModuleConfigValues.applyLegacy(json, moduleListVisibleValue, settings); - } - - /** - * Re-applies values to settings to trigger any side effects in Setting.load(). - */ - public void syncSettingsAfterLoad() { - ModuleConfigValues.syncAfterLoad(settings); - } - - public void applyConfiguredEnabledState() { - setEnabled(enabledValue.get(), activationSourceValue.get()); - } - - // Helpers for ModuleManager orchestration - public void loadAndApplyConfig() { - try { - DebugLog.config("Module config load -> %s", name()); - ConfigSerializer.load(this); - } catch (Throwable ignored) { - DebugLog.error("Module config load failed for %s", ignored, name()); - } - enabled = enabledValue.get(); - activationSource = normalizeActivationSource(enabled, activationSourceValue.get()); - activationSourceValue.set(activationSource); - if (enabled) { - if (ModuleExtensionManager.beforeEnable(this)) { - onEnable(); - ModuleExtensionManager.afterEnable(this); - } else { - enabled = false; - activationSource = ModuleActivationSource.NONE; - enabledValue.set(false); - activationSourceValue.set(activationSource); - } - } - } - - public void saveConfig() { - ModuleConfigValues.requestSaveQuietly(this); - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/Predictions.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/Predictions.java deleted file mode 100644 index 7ac825c..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/Predictions.java +++ /dev/null @@ -1,815 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.features.module.modules.combat.prediction; - -import com.mojang.blaze3d.pipeline.RenderPipeline; -import com.mojang.blaze3d.vertex.PoseStack; -import dev.loki.lovisual.features.module.core.Module; -import dev.loki.lovisual.features.module.core.ModuleCategory; -import dev.loki.lovisual.features.module.core.ModuleInfo; -import dev.loki.lovisual.features.module.modules.combat.prediction.Predictions; -import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsAim; -import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsSimulation; -import dev.loki.lovisual.features.module.modules.visuals.optimization.FpsOptimizer; -import dev.loki.lovisual.features.module.phase.HudPhase; -import dev.loki.lovisual.features.module.phase.WorldPhase; -import dev.loki.lovisual.features.module.core.Module; -import net.minecraft.client.Minecraft; -import net.minecraft.client.multiplayer.ClientLevel; -import net.minecraft.client.renderer.SubmitNodeCollector; -import net.minecraft.resources.Identifier; -import net.minecraft.util.Mth; -import net.minecraft.world.entity.Entity; -import net.minecraft.world.entity.LivingEntity; -import net.minecraft.world.entity.item.PrimedTnt; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.entity.projectile.FireworkRocketEntity; -import net.minecraft.world.entity.projectile.hurtingprojectile.Fireball; -import net.minecraft.world.entity.projectile.hurtingprojectile.windcharge.WindCharge; -import net.minecraft.world.entity.projectile.arrow.AbstractArrow; -import net.minecraft.world.entity.projectile.arrow.ThrownTrident; -import net.minecraft.world.entity.projectile.throwableitemprojectile.AbstractThrownPotion; -import net.minecraft.world.entity.projectile.throwableitemprojectile.Snowball; -import net.minecraft.world.entity.projectile.throwableitemprojectile.ThrownEgg; -import net.minecraft.world.entity.projectile.throwableitemprojectile.ThrownEnderpearl; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.Items; -import net.minecraft.world.phys.AABB; -import net.minecraft.world.phys.Vec3; -import org.joml.Quaternionf; -import dev.loki.lovisual.config.values.primitive.BooleanMapValue; -import dev.loki.lovisual.config.values.primitive.BooleanValue; -import dev.loki.lovisual.config.values.primitive.NumberValue; -import dev.loki.lovisual.config.values.color.RGBColorValue; -import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsAim; -import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsSimulation; -import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsModel.AimPrediction; -import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsModel.Kind; -import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsModel.TimerPlate; -import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsModel.TrajectoryResult; -import dev.loki.lovisual.mixins.accessors.projectile.PersistentProjectileEntityAccessor; -import dev.loki.lovisual.render.engine.RenderState; -import dev.loki.lovisual.render.engine.texture.TextureStorage; -import dev.loki.lovisual.render.engine.animation.AnimationUtility; -import dev.loki.lovisual.render.engine.color.RenderColor; -import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.renderer.Renderer3D; -import dev.loki.lovisual.render.engine.text.font.info.FontInfo; -import dev.loki.lovisual.render.engine.text.font.Fonts; -import dev.loki.lovisual.render.engine.text.render.TextRenderer; -import dev.loki.lovisual.render.engine.uniform.MeshBuilder; -import dev.loki.lovisual.render.helpers.clip.ScreenProjection; -import dev.loki.lovisual.util.target.TargetManager; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Locale; - -import static dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsGeometry.addBillboardQuad; -import static dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsGeometry.addGradientLine; -import static dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsGeometry.addImpactDecal; -import static dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsGeometry.addImpactRing; -import static dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsGeometry.multiplyAlpha; -import static dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsGeometry.withAlpha; -import static dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsSimulation.HIT_ENTITY_COLOR; -import static dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsSimulation.HIT_SELF_COLOR; - -//todo Description -@ModuleInfo(id = "predictions", displayName = "Predictions", category = ModuleCategory.COMBAT) -public class Predictions extends Module { - private static final String SETTING_LINE_COLOR = "line_color"; - private static final String SETTING_PEARL_CUSTOM_COLOR = "pearl_custom_color"; - private static final String SETTING_PEARL_COLOR = "pearl_color"; - private static final String SETTING_MAX_SIM_TICKS = "max_sim_ticks"; - private static final String SETTING_PEARL_MAX_SIM_TICKS = "pearl_max_ticks"; - private static final String SETTING_MAX_PROJECTILES = "max_projectiles"; - private static final String SETTING_PEARL_MAX_PROJECTILES = "pearl_max_projectiles"; - private static final String SETTING_SHOW_TIMER_PLATE = "show_plate"; - private static final String SETTING_AIM_TRAJECTORY = "aim_trajectory"; - private static final String SETTING_AIM_INDICATOR = "aim_indicator"; - private static final String SETTING_TARGETHUD_ON_AIM = "targethud_on_aim"; - private static final String SETTING_GLOW_WALLS = "glow_walls"; - private static final String SETTING_TNT_TIMER = "tnt_timer"; - private static final String SETTING_TNT_COLOR = "tnt_color"; - private static final String SETTING_PROJECTILES = "projectiles"; - - private static final int GLOW_SUBSTEPS = 10; - private static final float GLOW_CORE_SIZE = 0.085f; - private static final float GLOW_SOFT_SIZE = 0.16f; - private static final float GLOW_CORE_ALPHA = 0.74f; - private static final float GLOW_SOFT_ALPHA = 0.32f; - private static final int AIM_GLOW_SUBSTEPS = 7; - private static final float AIM_GLOW_CORE_SIZE = 0.055f; - private static final float AIM_GLOW_SOFT_SIZE = 0.105f; - private static final float AIM_GLOW_CORE_ALPHA = 0.42f; - private static final float AIM_GLOW_SOFT_ALPHA = 0.17f; - private static final double AIM_GLOW_MIN_CAMERA_DISTANCE_SQ = 0.56; - private static final float IMPACT_CORE_SIZE = 0.24f; - private static final float IMPACT_SOFT_SIZE = 0.48f; - private static final float IMPACT_CORE_ALPHA = 0.95f; - private static final float IMPACT_SOFT_ALPHA = 0.42f; - private static final double IMPACT_BOX_EXPAND = 0.03; - private static final int ENTITY_HIT_FILL_ALPHA = 34; - private static final int ENTITY_HIT_LINE_ALPHA = 170; - private static final float ENTITY_HIT_LINE_WIDTH = 1.5f; - private static final float PLATE_RADIUS = 1.7f; - private static final float PLATE_TEXT_SCALE = 0.68f; - private static final float PLATE_TEXT_PADDING = 24.0f; - private static final float PLATE_HEIGHT = 16.0f; - private static final float PLATE_BG_OFFSET_X = 2.0f; - private static final float PLATE_BG_OFFSET_Y = 0.0f; - private static final float PLATE_BG_SHRINK = 3.0f; - private static final float PLATE_ICON_SIZE = 13.0f; - private static final float PLATE_ICON_OFFSET_X = 4.0f; - private static final float PLATE_ICON_TEXT_GAP = 4.0f; - private static final float PLATE_TEXT_OFFSET_Y = 0.0f; - private static final int PLATE_SHADOW = 0xAA000000; - private static final float PLATE_SHADOW_BLUR = 3.6f; - private static final float PLATE_SHADOW_INNER_ALPHA = 0.18f; - private static final float TRAJECTORY_LINE_WIDTH = 1.35f; - private static final float TRAJECTORY_LINE_ALPHA_START = 0.16f; - private static final float TRAJECTORY_LINE_ALPHA_END = 0.78f; - private static final float IMPACT_DECAL_RADIUS = 0.34f; - private static final float IMPACT_RING_RADIUS = 0.30f; - private static final float IMPACT_RING_LINE_WIDTH = 1.2f; - private static final float IMPACT_RING_ALPHA = 0.88f; - - private final RGBColorValue lineColorValue = colorNoAlpha("pred_line_color", SETTING_LINE_COLOR, "#11C7C7"); - private final BooleanValue pearlColorToggle = bool("pred_pearl_custom_color", SETTING_PEARL_CUSTOM_COLOR, false); - private final RGBColorValue pearlColorValue = - visibleWhen(colorNoAlpha("pred_pearl_color", SETTING_PEARL_COLOR, "#C68CFF"), pearlColorToggle::get); - private final NumberValue maxSimTicks = - num("pred_max_ticks", SETTING_MAX_SIM_TICKS, 120, 20, 600); - private final NumberValue pearlMaxSimTicks = - num("pred_pearl_max_ticks", SETTING_PEARL_MAX_SIM_TICKS, 160, 20, 1000); - private final NumberValue maxProjectiles = - num("pred_max_projectiles", SETTING_MAX_PROJECTILES, 48, 1, 256); - private final NumberValue pearlMaxProjectiles = - num("pred_pearl_max_projectiles", SETTING_PEARL_MAX_PROJECTILES, 12, 1, 128); - private final BooleanValue showTimerPlateValue = bool("pred_show_timer_plate", SETTING_SHOW_TIMER_PLATE, true); - private final BooleanValue aimTrajectoryValue = bool("pred_aim_trajectory", SETTING_AIM_TRAJECTORY, true); - private final BooleanValue aimIndicatorValue = bool("pred_aim_indicator", SETTING_AIM_INDICATOR, false); - private final BooleanValue targetHudOnAimValue = bool("pred_targethud_on_aim", SETTING_TARGETHUD_ON_AIM, false); - private final BooleanValue glowWallsValue = bool("pred_glow_walls", SETTING_GLOW_WALLS, false); - private final BooleanValue tntTimerValue = bool("pred_tnt_timer", SETTING_TNT_TIMER, true); - private final RGBColorValue tntColorValue = - visibleWhen(colorNoAlpha("pred_tnt_color", SETTING_TNT_COLOR, "#FF5555"), tntTimerValue::get); - private final BooleanMapValue kindToggles = group("pred_kind_toggles", SETTING_PROJECTILES, new java.util.LinkedHashMap<>() {{ - put("arrow", true); - put("trident", true); - put("fireball", true); - put("wind_charge", true); - put("firework", true); - put("snowball", true); - put("egg", true); - put("potion_splash", true); - put("potion_lingering", true); - }}); - - private ItemStack tntTimerIcon = ItemStack.EMPTY; - private final List timerPlates = new ArrayList<>(); - - @Override - public WorldPhase getWorldPhase() { - return WorldPhase.END_MAIN; - } - - @Override - public HudPhase getHudPhase() { - return HudPhase.BEFORE_MISC_OVERLAYS; - } - - @Override - public void onDisable() { - TargetManager.setPredictionTarget(null); - } - - @Override - public void onRenderWorld(PoseStack matrices, SubmitNodeCollector consumers, float tickDelta) { - } - - @Override - public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { - Minecraft mc = Minecraft.getInstance(); - if (!isEnabled() || mc.level == null) { - TargetManager.setPredictionTarget(null); - return; - } - - List results = simulateProjectiles(tickDelta); - updatePredictionTarget(tickDelta); - - int baseColor = lineColorValue.getArgb(); - if (aimTrajectoryValue.get()) { - renderAimTrajectoryGlow(renderer, tickDelta, baseColor); - } - if (aimIndicatorValue.get()) { - renderAimIndicatorWorld(renderer, tickDelta); - } - - if (results.isEmpty()) return; - - renderLineTrajectories(renderer, results, baseColor); - renderImpactEffects(renderer, results, baseColor, tickDelta); - } - - @Override - public void onRenderHudEngine(Renderer2D renderer, TextRenderer textRenderer, net.minecraft.client.gui.GuiGraphicsExtractor ctx, float tickDelta) { - Minecraft mc = Minecraft.getInstance(); - if (!isEnabled() || mc.level == null) return; - - boolean showPlate = showTimerPlateValue.get(); - timerPlates.clear(); - List results = showPlate ? simulateProjectiles(tickDelta) : Collections.emptyList(); - if (showPlate && !results.isEmpty()) { - for (TrajectoryResult res : results) { - if (res.points().isEmpty()) continue; - Vec3 end = res.points().getLast(); - Vec3 screen = ScreenProjection.worldToScreen(end, tickDelta); - if (screen == null) continue; - drawTimerPlate(renderer, textRenderer, res, (float) screen.x, (float) screen.y); - } - } - - if (tntTimerValue.get()) { - drawTntTimers(renderer, textRenderer, tickDelta); - } - - } - - @Override - public void onRenderHudEngineForeground(Renderer2D renderer, TextRenderer textRenderer, net.minecraft.client.gui.GuiGraphicsExtractor ctx, float tickDelta) { - Minecraft mc = Minecraft.getInstance(); - if (!isEnabled() || mc.level == null) return; - if (timerPlates.isEmpty()) return; - - int seed = 0; - List plates = timerPlates.isEmpty() ? List.of() : new ArrayList<>(timerPlates); - for (TimerPlate plate : plates) { - if (plate.iconStack().isEmpty()) continue; - renderer.item( - plate.iconStack(), - plate.iconX(), - plate.iconY(), - plate.iconScale(), - seed++, - Renderer2D.ITEM_OVERLAY_NONE, - null - ); - } - if (!plates.isEmpty()) { - TextRenderer tr = Fonts.renderer("OnestMedium", FontInfo.Type.Regular, textRenderer); - tr.begin(PLATE_TEXT_SCALE); - for (TimerPlate plate : plates) { - tr.render(plate.text(), plate.textX(), plate.textY(), new RenderColor(0xFFFFFFFF), false); - } - tr.end(); - } - } - - private TrajectoryResult simulateEntity(Entity e, Kind kind, float tickDelta) { - return PredictionsSimulation.simulateEntity(e, kind, tickDelta, - maxSimTicks, pearlMaxSimTicks, pearlColorToggle, pearlColorValue); - } - - private AimPrediction computeAimPrediction(Player player, float tickDelta) { - return PredictionsAim.computeAimPrediction(player, tickDelta, - maxSimTicks, pearlMaxSimTicks, pearlColorToggle, pearlColorValue); - } - - private List simulateProjectiles(float tickDelta) { - Minecraft mc = Minecraft.getInstance(); - ClientLevel world = mc.level; - if (world == null) return Collections.emptyList(); - - List out = new ArrayList<>(); - - List candidates = new ArrayList<>(); - for (Entity e : world.entitiesForRendering()) { - candidates.add(e); - } - candidates.sort((a, b) -> { - double da = mc.player == null ? 0.0 : a.distanceToSqr(mc.player); - double db = mc.player == null ? 0.0 : b.distanceToSqr(mc.player); - return Double.compare(da, db); - }); - int limit = Math.max(1, maxProjectiles.get()); - int processed = 0; - int pearlProcessed = 0; - - for (Entity e : candidates) { - if (!e.isAlive()) continue; - if (!(e instanceof ThrownEnderpearl)) continue; - if (pearlProcessed >= pearlMaxProjectiles.get()) break; - - TrajectoryResult res = simulateEntity(e, Kind.PEARL_ENTITY, tickDelta); - if (res != null) out.add(res); - pearlProcessed++; - } - - for (Entity e : candidates) { - if (!e.isAlive()) continue; - if (e instanceof ThrownEnderpearl) continue; - if (processed >= limit) break; - - switch (e) { - case ThrownTrident trident -> { - if (isKindDisabled("trident")) continue; - if (trident.isNoPhysics() || isInGround(trident)) continue; - TrajectoryResult res = simulateEntity(trident, Kind.TRIDENT, tickDelta); - if (res != null) out.add(res); - processed++; - } - case AbstractArrow arrow -> { - if (isKindDisabled("arrow")) continue; - if (arrow.isNoPhysics() || isInGround(arrow)) continue; - TrajectoryResult res = simulateEntity(arrow, Kind.ARROW, tickDelta); - if (res != null) out.add(res); - processed++; - } - case Fireball fireball -> { - if (isKindDisabled("fireball")) continue; - TrajectoryResult res = simulateEntity(fireball, Kind.FIREBALL, tickDelta); - if (res != null) out.add(res); - processed++; - } - case WindCharge windCharge -> { - if (isKindDisabled("wind_charge")) continue; - TrajectoryResult res = simulateEntity(windCharge, Kind.WIND_CHARGE, tickDelta); - if (res != null) out.add(res); - processed++; - } - case FireworkRocketEntity firework -> { - if (isKindDisabled("firework")) continue; - TrajectoryResult res = simulateEntity(firework, Kind.FIREWORK, tickDelta); - if (res != null) out.add(res); - processed++; - } - case Snowball snowball -> { - if (isKindDisabled("snowball")) continue; - TrajectoryResult res = simulateEntity(snowball, Kind.SNOWBALL, tickDelta); - if (res != null) out.add(res); - processed++; - } - case ThrownEgg egg -> { - if (isKindDisabled("egg")) continue; - TrajectoryResult res = simulateEntity(egg, Kind.EGG, tickDelta); - if (res != null) out.add(res); - processed++; - } - case AbstractThrownPotion potion -> { - boolean lingering = potion.getItem().is(Items.LINGERING_POTION); - boolean splash = potion.getItem().is(Items.SPLASH_POTION) || !lingering; - if (lingering && isKindDisabled("potion_lingering")) continue; - if (splash && isKindDisabled("potion_splash")) continue; - Kind kind = lingering ? Kind.POTION_LINGERING : Kind.POTION_SPLASH; - TrajectoryResult res = simulateEntity(potion, kind, tickDelta); - if (res != null) out.add(res); - processed++; - } - default -> { - } - } - } - - return out; - } - - private boolean isKindDisabled(String key) { - // Ender pearls are always shown; caller must skip check for them. - return !kindToggles.get(key); - } - - private boolean isInGround(AbstractArrow arrow) { - if (arrow == null) return false; - try { - return ((PersistentProjectileEntityAccessor) arrow).lovisual$isInGround(); - } catch (Throwable t) { - return false; - } - } - - private void drawTimerPlate(Renderer2D renderer, TextRenderer textRenderer, TrajectoryResult res, float sx, float sy) { - double seconds = res.flightTicks() / 20.0; - if (seconds <= 0.0) return; - - ItemStack iconStack = useItemIcon(res.kind()) ? resolveTimerItem(res.kind()) : ItemStack.EMPTY; - Identifier iconId = iconStack.isEmpty() ? resolveTimerIcon(res.kind()) : null; - drawTimerPlate(renderer, textRenderer, formatTime(seconds), sx, sy, res.argb(), iconStack, iconId); - } - - private void drawTntTimers(Renderer2D renderer, TextRenderer textRenderer, float tickDelta) { - Minecraft mc = Minecraft.getInstance(); - ClientLevel world = mc.level; - if (world == null) return; - - int plateColor = tntColorValue.getArgb(); - for (Entity entity : world.entitiesForRendering()) { - if (!(entity instanceof PrimedTnt tnt) || !tnt.isAlive() || tnt.isRemoved()) continue; - - int fuse = tnt.getFuse(); - if (fuse < 0) continue; - - Vec3 anchor = tnt.getPosition(tickDelta).add(0.0, tnt.getBbHeight() + 0.5, 0.0); - Vec3 screen = ScreenProjection.worldToScreen(anchor, tickDelta); - if (screen == null) continue; - - double seconds = Math.max(0.0, (fuse - tickDelta) / 20.0); - drawTimerPlate( - renderer, - textRenderer, - formatTime(seconds), - (float) screen.x, - (float) screen.y, - plateColor, - tntTimerIcon(), - null - ); - } - } - - private ItemStack tntTimerIcon() { - if (tntTimerIcon.isEmpty()) { - tntTimerIcon = new ItemStack(Items.TNT); - } - return tntTimerIcon; - } - - private void drawTimerPlate(Renderer2D renderer, - TextRenderer textRenderer, - String txt, - float sx, - float sy, - int argb, - ItemStack iconStack, - Identifier iconId) { - if (txt == null || txt.isEmpty()) return; - - TextRenderer tr = Fonts.renderer("OnestMedium", FontInfo.Type.Regular, textRenderer); - float textW = (float) tr.getWidth(txt) * PLATE_TEXT_SCALE; - float plateW = textW + PLATE_TEXT_PADDING; - float plateX = sx - plateW * 0.5f; - float plateY = sy; - - float bgX = plateX + PLATE_BG_OFFSET_X; - float bgY = plateY + PLATE_BG_OFFSET_Y; - float bgW = plateW - PLATE_BG_SHRINK; - int plateShadow = argb != 0 - ? (argb & 0x00FFFFFF) | (PLATE_SHADOW & 0xFF000000) - : PLATE_SHADOW; - renderer.roundedRectSoftShadow(bgX, bgY, bgW, PLATE_HEIGHT, PLATE_RADIUS, PLATE_SHADOW_BLUR, PLATE_SHADOW_INNER_ALPHA, plateShadow); - - float iconX = plateX + PLATE_ICON_OFFSET_X; - float iconY = plateY + (PLATE_HEIGHT - PLATE_ICON_SIZE) * 0.5f; - float iconScale = PLATE_ICON_SIZE / 16.0f; - if (iconStack == null || iconStack.isEmpty()) { - iconStack = ItemStack.EMPTY; - drawTimerIcon(iconId, iconX, iconY, PLATE_ICON_SIZE); - } - - float textX = iconX + PLATE_ICON_SIZE + PLATE_ICON_TEXT_GAP; - float textH = (float) tr.getHeight() * PLATE_TEXT_SCALE; - float textY = plateY + (PLATE_HEIGHT - textH) * 0.5f + PLATE_TEXT_OFFSET_Y; - timerPlates.add(new TimerPlate(textX, textY, txt, iconX, iconY, iconScale, iconStack)); - } - - private void drawTimerIcon(Identifier iconId, float x, float y, float size) { - if (iconId == null) return; - Renderer2D.TEXTURE.roundedTexRect( - x, - y, - size, - size, - 1.5f, - 1.0f, - 0, - 0, - 1, - 1, - 0xFFFFFFFF, - iconId - ); - } - - private boolean useItemIcon(Kind kind) { - return kind == Kind.SNOWBALL - || kind == Kind.EGG - || kind == Kind.FIREBALL - || kind == Kind.FIREWORK - || kind == Kind.WIND_CHARGE; - } - - private ItemStack resolveTimerItem(Kind kind) { - return switch (kind) { - case SNOWBALL -> new ItemStack(Items.SNOWBALL); - case EGG -> new ItemStack(Items.EGG); - case FIREBALL -> new ItemStack(Items.FIRE_CHARGE); - case FIREWORK -> new ItemStack(Items.FIREWORK_ROCKET); - case WIND_CHARGE -> new ItemStack(Items.WIND_CHARGE); - default -> ItemStack.EMPTY; - }; - } - - private Identifier resolveTimerIcon(Kind kind) { - return switch (kind) { - case PEARL_ENTITY -> Identifier.fromNamespaceAndPath("lovisual", "textures/timers/pearl.png"); - case TRIDENT -> Identifier.fromNamespaceAndPath("lovisual", "textures/timers/trident.png"); - case POTION_SPLASH, POTION_LINGERING -> - Identifier.fromNamespaceAndPath("lovisual", "textures/timers/potion.png"); - case ARROW -> Identifier.fromNamespaceAndPath("lovisual", "textures/timers/arrow.png"); - case FIREBALL, FIREWORK, WIND_CHARGE, SNOWBALL, EGG -> null; - }; - } - - private void renderAimTrajectoryGlow(Renderer3D renderer, float tickDelta, int baseColor) { - Minecraft mc = Minecraft.getInstance(); - Player player = mc.player; - if (player == null) return; - - AimPrediction pred = computeAimPrediction(player, tickDelta); - if (pred == null || pred.trajectory() == null) return; - - List points = pred.trajectory().points(); - if (points.size() < 2) return; - - boolean walls = glowWallsValue.get(); - RenderPipeline pipeline = walls - ? LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE - : LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE_LIQUID_IGNORE; - Renderer3D.DepthMode depthMode = walls ? Renderer3D.DepthMode.MAIN : Renderer3D.DepthMode.PRE_DEPTH; - if (FpsOptimizer.bloomDisabled()) return; - MeshBuilder mesh = renderer.batchTextured(pipeline, TextureStorage.BLOOM, depthMode); - if (mesh == null) return; - - int color = pred.entity() != null - ? HIT_ENTITY_COLOR - : pred.trajectory().argb() != 0 ? pred.trajectory().argb() : baseColor; - Quaternionf camRot = RenderState.cameraRotation; - Vec3 cameraPos = RenderState.cameraPos != null ? RenderState.cameraPos : player.getEyePosition(tickDelta); - int totalSegments = points.size() - 1; - - for (int i = 1; i < points.size(); i++) { - Vec3 from = points.get(i - 1); - Vec3 to = points.get(i); - - for (int step = 0; step <= AIM_GLOW_SUBSTEPS; step++) { - float stepT = step / (float) AIM_GLOW_SUBSTEPS; - Vec3 pos = from.lerp(to, stepT); - if (pos.distanceToSqr(cameraPos) < AIM_GLOW_MIN_CAMERA_DISTANCE_SQ) { - continue; - } - float segmentProgress = ((i - 1) + stepT) / Math.max(1.0f, totalSegments); - float strength = 0.28f + 0.72f * AnimationUtility.easeOutCubic(segmentProgress); - - addBillboardQuad( - mesh, - pos.x, pos.y, pos.z, - AIM_GLOW_CORE_SIZE, - camRot, - multiplyAlpha(color, AIM_GLOW_CORE_ALPHA * strength) - ); - addBillboardQuad( - mesh, - pos.x, pos.y, pos.z, - AIM_GLOW_SOFT_SIZE, - camRot, - multiplyAlpha(color, AIM_GLOW_SOFT_ALPHA * strength) - ); - } - } - } - - private void renderAimIndicatorWorld(Renderer3D renderer, float tickDelta) { - Minecraft mc = Minecraft.getInstance(); - Player player = mc.player; - if (player == null) return; - - AimPrediction pred = computeAimPrediction(player, tickDelta); - if (pred == null) return; - - RenderPipeline pipeline = LoVisualRenderPipelines.WORLD_COLORED_LINES_LIQUID_IGNORE; - Renderer3D.DepthMode depthMode = Renderer3D.DepthMode.NONE; - - float prevWidth = RenderState.lineWidth; - RenderState.lineWidth = IMPACT_RING_LINE_WIDTH; - try { - MeshBuilder mesh = renderer.batch(pipeline, depthMode); - if (mesh == null) return; - - int color = pred.entity() != null ? HIT_ENTITY_COLOR : lineColorValue.getArgb(); - float pulseTime = (mc.level.getGameTime() + tickDelta) * 0.085f; - float pulse = 0.9f + 0.1f * AnimationUtility.smoothstep((float) ((Math.sin(pulseTime) + 1.0) * 0.5)); - Vec3 normal = pred.hitNormal() != null ? pred.hitNormal() : new Vec3(0.0, 1.0, 0.0); - addImpactRing(mesh, pred.hitPos(), normal, IMPACT_RING_RADIUS * pulse, multiplyAlpha(color, IMPACT_RING_ALPHA)); - } finally { - RenderState.lineWidth = prevWidth; - } - } - - private void updatePredictionTarget(float tickDelta) { - Minecraft mc = Minecraft.getInstance(); - if (!targetHudOnAimValue.get()) { - TargetManager.setPredictionTarget(null); - return; - } - - Player player = mc.player; - if (player == null || mc.level == null) { - TargetManager.setPredictionTarget(null); - return; - } - - AimPrediction pred = computeAimPrediction(player, tickDelta); - if (pred != null && pred.entity() instanceof LivingEntity living && living.isAlive() && !living.isRemoved()) { - TargetManager.setPredictionTarget(living); - return; - } - - TargetManager.setPredictionTarget(null); - } - - private String formatTime(double seconds) { - String s = String.format(Locale.US, "%.1f", seconds); - s = s.replace('.', ','); - return s + " s"; - } - - private void renderGlowTrajectories(Renderer3D renderer, List results, int baseColor) { - boolean walls = glowWallsValue.get(); - RenderPipeline pipeline = walls - ? LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE - : LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE_LIQUID_IGNORE; - Renderer3D.DepthMode depthMode = walls ? Renderer3D.DepthMode.MAIN : Renderer3D.DepthMode.PRE_DEPTH; - if (FpsOptimizer.bloomDisabled()) return; - MeshBuilder mesh = renderer.batchTextured(pipeline, TextureStorage.BLOOM, depthMode); - if (mesh == null) return; - - Quaternionf camRot = RenderState.cameraRotation; - for (TrajectoryResult res : results) { - List points = res.points(); - if (points.size() < 2) continue; - - int color = res.argb() != 0 ? res.argb() : baseColor; - int totalSegments = points.size() - 1; - - for (int i = 1; i < points.size(); i++) { - Vec3 from = points.get(i - 1); - Vec3 to = points.get(i); - - for (int step = 0; step <= GLOW_SUBSTEPS; step++) { - float stepT = step / (float) GLOW_SUBSTEPS; - Vec3 pos = from.lerp(to, stepT); - float segmentProgress = ((i - 1) + stepT) / Math.max(1.0f, totalSegments); - float strength = 0.45f + 0.55f * AnimationUtility.easeOutCubic(segmentProgress); - float sizeBoost = 0.92f + 0.25f * strength; - - addBillboardQuad( - mesh, - pos.x, pos.y, pos.z, - GLOW_CORE_SIZE * sizeBoost, - camRot, - multiplyAlpha(color, GLOW_CORE_ALPHA * strength) - ); - addBillboardQuad( - mesh, - pos.x, pos.y, pos.z, - GLOW_SOFT_SIZE * sizeBoost, - camRot, - multiplyAlpha(color, GLOW_SOFT_ALPHA * strength) - ); - } - } - } - } - - private void renderLineTrajectories(Renderer3D renderer, List results, int baseColor) { - boolean walls = glowWallsValue.get(); - RenderPipeline pipeline = walls - ? LoVisualRenderPipelines.WORLD_COLORED_LINES_LIQUID_BLEND - : LoVisualRenderPipelines.WORLD_COLORED_LINES_LIQUID_IGNORE; - Renderer3D.DepthMode depthMode = walls ? Renderer3D.DepthMode.MAIN : Renderer3D.DepthMode.PRE_DEPTH; - - float prevWidth = RenderState.lineWidth; - RenderState.lineWidth = TRAJECTORY_LINE_WIDTH; - try { - MeshBuilder mesh = renderer.batch(pipeline, depthMode); - if (mesh == null) return; - - for (TrajectoryResult res : results) { - List points = res.points(); - if (points.size() < 2) continue; - - int color = res.argb() != 0 ? res.argb() : baseColor; - int totalSegments = points.size() - 1; - for (int i = 1; i < points.size(); i++) { - Vec3 from = points.get(i - 1); - Vec3 to = points.get(i); - - float startT = (i - 1) / (float) totalSegments; - float endT = i / (float) totalSegments; - float startAlpha = Mth.lerp(AnimationUtility.easeOutCubic(startT), TRAJECTORY_LINE_ALPHA_START, TRAJECTORY_LINE_ALPHA_END); - float endAlpha = Mth.lerp(AnimationUtility.easeOutCubic(endT), TRAJECTORY_LINE_ALPHA_START, TRAJECTORY_LINE_ALPHA_END); - - addGradientLine(mesh, from, to, multiplyAlpha(color, startAlpha), multiplyAlpha(color, endAlpha)); - } - } - } finally { - RenderState.lineWidth = prevWidth; - } - } - - private void renderImpactEffects(Renderer3D renderer, List results, int baseColor, float tickDelta) { - Minecraft mc = Minecraft.getInstance(); - boolean walls = glowWallsValue.get(); - RenderPipeline linePipeline = walls - ? LoVisualRenderPipelines.WORLD_COLORED_LINES_LIQUID_BLEND - : LoVisualRenderPipelines.WORLD_COLORED_LINES_LIQUID_IGNORE; - Renderer3D.DepthMode lineDepthMode = walls ? Renderer3D.DepthMode.MAIN : Renderer3D.DepthMode.PRE_DEPTH; - RenderPipeline decalPipeline = walls - ? LoVisualRenderPipelines.WORLD_DECAL_SDF - : LoVisualRenderPipelines.WORLD_DECAL_SDF_DEPTH; - Renderer3D.DepthMode decalDepthMode = walls ? Renderer3D.DepthMode.NONE : Renderer3D.DepthMode.PRE_DEPTH; - float prevWidth = RenderState.lineWidth; - RenderState.lineWidth = IMPACT_RING_LINE_WIDTH; - try { - MeshBuilder lineMesh = renderer.batch(linePipeline, lineDepthMode); - MeshBuilder decalMesh = renderer.batch(decalPipeline, decalDepthMode); - for (TrajectoryResult res : results) { - Vec3 impact = res.impactPos(); - if (impact != null && decalMesh != null) { - int color = res.argb() != 0 ? res.argb() : baseColor; - float pulseTime = (mc.level.getGameTime() + tickDelta) * 0.085f; - float pulse = 0.85f + 0.15f * AnimationUtility.smoothstep((float) ((Math.sin(pulseTime) + 1.0) * 0.5)); - Vec3 normal = res.impactNormal() != null ? res.impactNormal() : new Vec3(0.0, 1.0, 0.0); - addImpactDecal(decalMesh, impact, normal, IMPACT_DECAL_RADIUS * pulse, color); - } - - if (res.hitEntity() != null && res.hitEntity().isAlive()) { - int color = res.argb() != 0 ? res.argb() : baseColor; - AABB box = res.hitEntity().getBoundingBox().inflate(IMPACT_BOX_EXPAND); - addFilledBox(renderer, box, withAlpha(color, ENTITY_HIT_FILL_ALPHA)); - addOutlineBox(renderer, box, withAlpha(color, ENTITY_HIT_LINE_ALPHA)); - } - } - } finally { - RenderState.lineWidth = prevWidth; - } - } - - private static void addFilledBox(Renderer3D renderer, AABB box, int argb) { - int a = (argb >>> 24) & 0xFF; - int r = (argb >>> 16) & 0xFF; - int g = (argb >>> 8) & 0xFF; - int b = argb & 0xFF; - - double minX = box.minX; - double minY = box.minY; - double minZ = box.minZ; - double maxX = box.maxX; - double maxY = box.maxY; - double maxZ = box.maxZ; - - renderer.quad(minX, minY, minZ, maxX, minY, minZ, maxX, minY, maxZ, minX, minY, maxZ, r, g, b, a); - renderer.quad(minX, maxY, minZ, minX, maxY, maxZ, maxX, maxY, maxZ, maxX, maxY, minZ, r, g, b, a); - renderer.quad(minX, minY, maxZ, maxX, minY, maxZ, maxX, maxY, maxZ, minX, maxY, maxZ, r, g, b, a); - renderer.quad(minX, minY, minZ, minX, maxY, minZ, maxX, maxY, minZ, maxX, minY, minZ, r, g, b, a); - renderer.quad(maxX, minY, minZ, maxX, maxY, minZ, maxX, maxY, maxZ, maxX, minY, maxZ, r, g, b, a); - renderer.quad(minX, minY, minZ, minX, minY, maxZ, minX, maxY, maxZ, minX, maxY, minZ, r, g, b, a); - } - - private static void addOutlineBox(Renderer3D renderer, AABB box, int argb) { - int a = (argb >>> 24) & 0xFF; - int r = (argb >>> 16) & 0xFF; - int g = (argb >>> 8) & 0xFF; - int b = argb & 0xFF; - - double minX = box.minX; - double minY = box.minY; - double minZ = box.minZ; - double maxX = box.maxX; - double maxY = box.maxY; - double maxZ = box.maxZ; - - renderer.line(minX, minY, minZ, maxX, minY, minZ, r, g, b, a); - renderer.line(maxX, minY, minZ, maxX, minY, maxZ, r, g, b, a); - renderer.line(maxX, minY, maxZ, minX, minY, maxZ, r, g, b, a); - renderer.line(minX, minY, maxZ, minX, minY, minZ, r, g, b, a); - - renderer.line(minX, maxY, minZ, maxX, maxY, minZ, r, g, b, a); - renderer.line(maxX, maxY, minZ, maxX, maxY, maxZ, r, g, b, a); - renderer.line(maxX, maxY, maxZ, minX, maxY, maxZ, r, g, b, a); - renderer.line(minX, maxY, maxZ, minX, maxY, minZ, r, g, b, a); - - renderer.line(minX, minY, minZ, minX, maxY, minZ, r, g, b, a); - renderer.line(maxX, minY, minZ, maxX, maxY, minZ, r, g, b, a); - renderer.line(maxX, minY, maxZ, maxX, maxY, maxZ, r, g, b, a); - renderer.line(minX, minY, maxZ, minX, maxY, maxZ, r, g, b, a); - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/ProjectilePuncher.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/ProjectilePuncher.java deleted file mode 100644 index 713f3b5..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/ProjectilePuncher.java +++ /dev/null @@ -1,747 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.features.module.modules.combat.prediction; - -import dev.loki.lovisual.util.aiming.features.processors.RotationProcessor; -import net.minecraft.client.Minecraft; -import net.minecraft.util.Mth; -import net.minecraft.world.InteractionHand; -import net.minecraft.world.entity.Entity; -import net.minecraft.world.entity.EntityType; -import net.minecraft.world.phys.AABB; -import net.minecraft.world.phys.Vec3; -import dev.loki.lovisual.config.common.CommonSettingSchemas; -import dev.loki.lovisual.config.values.primitive.BooleanMapValue; -import dev.loki.lovisual.config.values.primitive.BooleanValue; -import dev.loki.lovisual.config.values.mode.EnumValue; -import dev.loki.lovisual.config.values.primitive.NumberValue; -import dev.loki.lovisual.events.meta.EventHandler; -import dev.loki.lovisual.events.impl.input.RotationUpdateEvent; -import dev.loki.lovisual.features.module.core.Module; -import dev.loki.lovisual.features.module.core.ModuleCategory; -import dev.loki.lovisual.features.module.core.ModuleInfo; -import dev.loki.lovisual.util.aiming.rotation.RotationManager; -import dev.loki.lovisual.util.aiming.rotation.RotationTarget; -import dev.loki.lovisual.util.aiming.data.Rotation; -import dev.loki.lovisual.util.aiming.data.RotationWithVector; -import dev.loki.lovisual.util.aiming.features.MovementCorrection; -import dev.loki.lovisual.util.aiming.features.processors.anglesmooth.AngleSmooth; -import dev.loki.lovisual.util.aiming.features.processors.anglesmooth.impl.LinearAngleSmooth; -import dev.loki.lovisual.util.aiming.features.processors.anglesmooth.impl.curves.SigmoidAngleSmooth; -import dev.loki.lovisual.util.aiming.features.processors.anglesmooth.impl.curves.SmartAngleSmooth; -import dev.loki.lovisual.util.aiming.features.processors.anglesmooth.impl.curves.SpookyAngleSmooth; -import dev.loki.lovisual.util.aiming.preference.LeastDifferencePreference; -import dev.loki.lovisual.util.aiming.preference.RotationPreference; -import dev.loki.lovisual.util.aiming.raytrace.RotationRaytrace; -import dev.loki.lovisual.util.click.ClickScheduler; -import dev.loki.lovisual.util.raycast.RaycastUtil; -import dev.loki.lovisual.util.target.TargetingUtil; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.Map; -import dev.loki.lovisual.config.common.impl.wall.RotationTiming; - -//todo Description -@ModuleInfo( - id = "projectilepuncher", - displayName = "ProjectilePuncher", - category = ModuleCategory.COMBAT -) -public final class ProjectilePuncher extends Module { - - public enum RaycastMode { - STRICT, - THROUGH_WALLS, - NONE - } - - public enum RotationTiming { - NORMAL, - SNAP, - ON_TICK - } - - public enum AngleSmoothMode { - LINEAR, - SIGMOID, - SMART, - SPOOKY, - NONE - } - - private static final String PROJECTILE_FIREBALL = "fireball"; - private static final String PROJECTILE_SHULKER_BULLET = "shulker_bullet"; - private static final int ROTATION_PRIORITY = 10; - private final NumberValue range = - numCommon( - "projectilePuncherRange", - "range", - CommonSettingSchemas.COMBAT_RANGE, - 3.0, - 2.0, - 6.0 - ); - private final NumberValue wallRange = - numCommon( - "projectilePuncherWallRange", - "wall_range", - CommonSettingSchemas.COMBAT_WALL_RANGE, - 0.0, - 0.0, - 6.0 - ); - private final BooleanMapValue projectiles = - common( - group( - "projectilePuncherProjectiles", - "projectiles", - projectileDefaults() - ), - CommonSettingSchemas.ITEMS_PROJECTILES.commonI18nKey() - ); - private final BooleanValue ignoreOpenInventory = - boolCommon( - "projectilePuncherIgnoreOpenInventory", - "ignore_open_inventory", - CommonSettingSchemas.INTERACTION_IGNORE_OPEN_INVENTORY, - true - ); - private final EnumValue raycast = - enumCommon( - "projectilePuncherRaycast", - "raycast", - CommonSettingSchemas.COMBAT_RAYCAST, - RaycastMode.STRICT, - RaycastMode.values() - ); - private final EnumValue rotationTiming = - enumCommon( - "projectilePuncherRotationTiming", - "rotation_timing", - CommonSettingSchemas.ROTATION_TIMING, - RotationTiming.NORMAL, - RotationTiming.values() - ); - private final EnumValue movementCorrection = - enumCommon( - "projectilePuncherMovementCorrection", - "movement_correction", - CommonSettingSchemas.ROTATION_MOVEMENT_CORRECTION, - MovementCorrection.STRICT, - MovementCorrection.values() - ); - private final EnumValue rotationMode = - enumCommon( - "projectilePuncherRotationMode", - "rotation_mode", - CommonSettingSchemas.ROTATION_MODE, - AngleSmoothMode.SMART, - AngleSmoothMode.values() - ); - private final NumberValue rotationResetTicks = - numCommon( - "projectilePuncherRotationResetTicks", - "rotation_reset_ticks", - CommonSettingSchemas.ROTATION_RESET_TICKS, - 2, - 1, - 20 - ); - private final NumberValue rotationResetThreshold = - numCommon( - "projectilePuncherRotationResetThreshold", - "rotation_reset_threshold", - CommonSettingSchemas.ROTATION_RESET_THRESHOLD, - 2.0f, - 0.1f, - 15.0f - ); - private final BooleanValue rotationThroughWalls = - boolCommon( - "projectilePuncherRotationThroughWalls", - "rotation_through_walls", - CommonSettingSchemas.ROTATION_THROUGH_WALLS, - false - ); - private final NumberValue predictTicks = - num( - "projectilePuncherPredictTicks", - "predict_ticks", - 1, - 0, - 5 - ); - private final NumberValue cpsMin = - numCommon( - "projectilePuncherCpsMin", - "cps_min", - CommonSettingSchemas.COMBAT_CPS_MIN, - 20, - 1, - 20 - ); - private final NumberValue cpsMax = - numCommon( - "projectilePuncherCpsMax", - "cps_max", - CommonSettingSchemas.COMBAT_CPS_MAX, - 20, - 1, - 20 - ); - private final ClickScheduler clicker = new ClickScheduler(cpsMin.get(), cpsMax.get()); - private final NumberValue linearHorMin = - numCommon( - "projectilePuncherLinearHorizontalMin", - "linear_horizontal_min", - CommonSettingSchemas.ANGLESMOOTH_HORIZONTAL_MIN, - 180f, - 0f, - 180f - ); - private final NumberValue linearHorMax = - numCommon( - "projectilePuncherLinearHorizontalMax", - "linear_horizontal_max", - CommonSettingSchemas.ANGLESMOOTH_HORIZONTAL_MAX, - 180f, - 0f, - 180f - ); - private final NumberValue linearVertMin = - numCommon( - "projectilePuncherLinearVerticalMin", - "linear_vertical_min", - CommonSettingSchemas.ANGLESMOOTH_VERTICAL_MIN, - 180f, - 0f, - 180f - ); - private final NumberValue linearVertMax = - numCommon( - "projectilePuncherLinearVerticalMax", - "linear_vertical_max", - CommonSettingSchemas.ANGLESMOOTH_VERTICAL_MAX, - 180f, - 0f, - 180f - ); - private final NumberValue sigmoidHorMin = - numCommon( - "projectilePuncherSigmoidHorizontalMin", - "sigmoid_horizontal_min", - CommonSettingSchemas.ANGLESMOOTH_HORIZONTAL_MIN, - 180f, - 0f, - 180f - ); - private final NumberValue sigmoidHorMax = - numCommon( - "projectilePuncherSigmoidHorizontalMax", - "sigmoid_horizontal_max", - CommonSettingSchemas.ANGLESMOOTH_HORIZONTAL_MAX, - 180f, - 0f, - 180f - ); - private final NumberValue sigmoidVertMin = - numCommon( - "projectilePuncherSigmoidVerticalMin", - "sigmoid_vertical_min", - CommonSettingSchemas.ANGLESMOOTH_VERTICAL_MIN, - 180f, - 0f, - 180f - ); - private final NumberValue sigmoidVertMax = - numCommon( - "projectilePuncherSigmoidVerticalMax", - "sigmoid_vertical_max", - CommonSettingSchemas.ANGLESMOOTH_VERTICAL_MAX, - 180f, - 0f, - 180f - ); - private final NumberValue sigmoidSteepness = - numCommon( - "projectilePuncherSigmoidSteepness", - "sigmoid_steepness", - CommonSettingSchemas.ANGLESMOOTH_STEEPNESS, - 10f, - 0f, - 20f - ); - private final NumberValue sigmoidMidpoint = - numCommon( - "projectilePuncherSigmoidMidpoint", - "sigmoid_midpoint", - CommonSettingSchemas.ANGLESMOOTH_MIDPOINT, - 0.3f, - 0f, - 1f - ); - private final NumberValue smartYawMin = - numCommon( - "projectilePuncherSmartYawMin", - "smart_yaw_min", - CommonSettingSchemas.ANGLESMOOTH_YAW_MIN, - 70f, - 1f, - 180f - ); - private final NumberValue smartYawMax = - numCommon( - "projectilePuncherSmartYawMax", - "smart_yaw_max", - CommonSettingSchemas.ANGLESMOOTH_YAW_MAX, - 95f, - 1f, - 180f - ); - private final NumberValue smartPitchMin = - numCommon( - "projectilePuncherSmartPitchMin", - "smart_pitch_min", - CommonSettingSchemas.ANGLESMOOTH_PITCH_MIN, - 45f, - 1f, - 180f - ); - private final NumberValue smartPitchMax = - numCommon( - "projectilePuncherSmartPitchMax", - "smart_pitch_max", - CommonSettingSchemas.ANGLESMOOTH_PITCH_MAX, - 70f, - 1f, - 180f - ); - private final NumberValue smartSnapThreshold = - numCommon( - "projectilePuncherSmartSnapThreshold", - "smart_snap_threshold", - CommonSettingSchemas.ANGLESMOOTH_SNAP_THRESHOLD, - 1.2f, - 0.05f, - 10.0f - ); - private final NumberValue smartJitterYaw = - numCommon( - "projectilePuncherSmartJitterYaw", - "smart_jitter_yaw", - CommonSettingSchemas.ANGLESMOOTH_JITTER_YAW, - 0.8f, - 0.0f, - 6.0f - ); - private final NumberValue smartJitterPitch = - numCommon( - "projectilePuncherSmartJitterPitch", - "smart_jitter_pitch", - CommonSettingSchemas.ANGLESMOOTH_JITTER_PITCH, - 0.4f, - 0.0f, - 4.0f - ); - private final BooleanValue smartDecelerateEnabled = - boolCommon( - "projectilePuncherSmartDecelerateEnabled", - "smart_decelerate_enabled", - CommonSettingSchemas.ANGLESMOOTH_DECELERATE_ENABLED, - true - ); - private final NumberValue smartDecelerateAngle = - numCommon( - "projectilePuncherSmartDecelerateAngle", - "smart_decelerate_angle", - CommonSettingSchemas.ANGLESMOOTH_DECELERATE_ANGLE, - 35.0f, - 1.0f, - 120.0f - ); - private final NumberValue smartDecelerateMinFactor = - numCommon( - "projectilePuncherSmartDecelerateMinFactor", - "smart_decelerate_min_factor", - CommonSettingSchemas.ANGLESMOOTH_DECELERATE_MIN_FACTOR, - 0.22f, - 0.05f, - 1.0f - ); - private final LinearAngleSmooth linearSmooth = new LinearAngleSmooth( - linearHorMin, linearHorMax, linearVertMin, linearVertMax - ); - private final SigmoidAngleSmooth sigmoidSmooth = new SigmoidAngleSmooth( - sigmoidHorMin, sigmoidHorMax, sigmoidVertMin, sigmoidVertMax, - sigmoidSteepness, sigmoidMidpoint - ); - private final SmartAngleSmooth smartSmooth = new SmartAngleSmooth( - smartYawMin, smartYawMax, smartPitchMin, smartPitchMax, - smartSnapThreshold, smartJitterYaw, smartJitterPitch, - smartDecelerateEnabled, smartDecelerateAngle, smartDecelerateMinFactor - ); - private final SpookyAngleSmooth spookySmooth = new SpookyAngleSmooth(); - private Entity currentTarget; - private Vec3 lastAimPoint; - private int lastAimEntityId = -1; - private Rotation lastRotation; - private int lastRotationTick = -1; - private int lastAttackExecutionAge = -1; - private boolean rotationReleased = true; - - private static Map projectileDefaults() { - return Map.of( - PROJECTILE_FIREBALL, true, - PROJECTILE_SHULKER_BULLET, true - ); - } - - private static Vec3 clampToBox(Vec3 point, AABB box) { - return new Vec3( - Mth.clamp(point.x, box.minX, box.maxX), - Mth.clamp(point.y, box.minY, box.maxY), - Mth.clamp(point.z, box.minZ, box.maxZ) - ); - } - - @Override - public void onEnable() { - clicker.reset(); - currentTarget = null; - lastRotation = null; - lastRotationTick = -1; - lastAttackExecutionAge = -1; - rotationReleased = true; - } - - @Override - public void onDisable() { - clearTargetImmediate(); - } - - @Override - public void onTick() { - Minecraft mc = Minecraft.getInstance(); - if (mc.player == null || mc.level == null) { - clearTargetImmediate(); - } - } - - @EventHandler(priority = ROTATION_PRIORITY) - public void onRotationUpdate(RotationUpdateEvent event) { - Minecraft mc = Minecraft.getInstance(); - if (!isEnabled()) return; - if (mc.player == null || mc.level == null) { - clearTargetImmediate(); - return; - } - - if (event.getType() == RotationUpdateEvent.Type.POST) { - tryAttackPost(); - return; - } - - clicker.setCps(cpsMin.get(), cpsMax.get()); - clicker.tick(); - - List candidates = findCandidates(); - for (Entity entity : candidates) { - if (processTarget(entity, range.get().floatValue(), wallRange.get().floatValue())) { - currentTarget = entity; - return; - } - } - - clearTarget(); - } - - private void tryAttackPost() { - Minecraft mc = Minecraft.getInstance(); - if (mc.player == null || mc.level == null || mc.gameMode == null) return; - if (mc.player.tickCount == lastAttackExecutionAge) { - return; - } - - Entity target = currentTarget; - if (target == null || target.isRemoved()) return; - if (!isInRange(target)) return; - - Rotation rotation = resolveAttackRotation(target); - if (rotation == null) { - clearTarget(); - return; - } - if (!passesRaycast(target, rotation.yaw(), rotation.pitch())) { - return; - } - - int clickCount = clicker.getClicksAt(0); - if (clickCount <= 0) return; - - lastAttackExecutionAge = mc.player.tickCount; - for (int i = 0; i < clickCount; i++) { - if (target.isRemoved()) { - break; - } - mc.gameMode.attack(mc.player, target); - mc.player.swing(InteractionHand.MAIN_HAND); - } - } - - private List findCandidates() { - Minecraft mc = Minecraft.getInstance(); - double searchRange = Math.max(range.get(), wallRange.get()) + 8.0; - List candidates = mc.level.getEntities( - mc.player, - mc.player.getBoundingBox().inflate(searchRange), - this::isSupportedProjectile - ); - - candidates.removeIf(entity -> !shouldAttack(entity)); - candidates.sort(Comparator.comparingDouble(this::predictedDistanceSq)); - return candidates; - } - - private boolean processTarget(Entity target, float range, float wallsRange) { - Minecraft mc = Minecraft.getInstance(); - RotationWithVector rotation = findRotation(target, range, wallsRange); - if (rotation == null) return false; - - lastRotation = rotation.rotation(); - lastRotationTick = mc.player != null ? mc.player.tickCount : -1; - rotationReleased = false; - - RotationTiming timing = rotationTiming.get(); - int rotTicks = calculateTicks(rotation.rotation()); - - if (timing == RotationTiming.SNAP && !clicker.willClickAt(Math.max(1, rotTicks))) { - return true; - } - - RotationTarget plan = buildRotationTarget(rotation.rotation(), target); - RotationManager.INSTANCE.setRotationTarget(plan, ROTATION_PRIORITY, this); - return true; - } - - private Rotation resolveAttackRotation(Entity target) { - Minecraft mc = Minecraft.getInstance(); - if (mc.player == null) return null; - - Rotation rotation; - RotationTiming timing = rotationTiming.get(); - if (timing == RotationTiming.ON_TICK) { - rotation = lastRotationTick == mc.player.tickCount ? lastRotation : null; - if (rotation == null) { - RotationWithVector rot = findRotation(target, range.get().floatValue(), wallRange.get().floatValue()); - rotation = rot != null ? rot.rotation() : null; - if (rotation == null) { - return null; - } - } - } else { - Rotation current = RotationManager.INSTANCE.getCurrentRotation(); - rotation = current != null ? current : new Rotation(mc.player.getYRot(), mc.player.getXRot(), true); - } - - return rotation.normalize(); - } - - private boolean passesRaycast(Entity target, float yaw, float pitch) { - Minecraft mc = Minecraft.getInstance(); - RaycastMode mode = raycast.get(); - if (mode == RaycastMode.NONE) return true; - - double maxRange = range.get(); - if (mode == RaycastMode.THROUGH_WALLS) { - return RaycastUtil.isLookingAtEntity(mc.player, target, yaw, pitch, maxRange, wallRange.get()) != null; - } - - return RaycastUtil.isLookingAtEntity(mc.player, target, yaw, pitch, maxRange, 0.0) != null; - } - - private boolean isInRange(Entity target) { - Minecraft mc = Minecraft.getInstance(); - return TargetingUtil.distanceToEntityBoxSq(mc.player.getEyePosition(), target) <= range.get() * range.get(); - } - - private RotationWithVector findRotation(Entity target, float range, float wallsRange) { - Minecraft mc = Minecraft.getInstance(); - if (mc.player == null) return null; - - Vec3 eyes = mc.player.getEyePosition(); - AABB targetBox = predictBox(target); - RotationPreference preference = lastAimEntityId == target.getId() && lastAimPoint != null - ? LeastDifferencePreference.leastDifferenceToLastPoint(eyes, lastAimPoint) - : LeastDifferencePreference.LEAST_DISTANCE_TO_CURRENT_ROTATION; - - RotationWithVector rot = RotationRaytrace.raytraceBox( - eyes, - targetBox, - range, - wallsRange, - RotationRaytrace.OUTLINE_VISIBILITY, - preference, - true - ); - - if (rot == null && rotationThroughWalls.get()) { - rot = RotationRaytrace.raytraceBox( - eyes, - targetBox, - range, - range, - RotationRaytrace.OUTLINE_VISIBILITY, - preference, - true - ); - } - - if (rot != null) { - rot = stabilizeRotationResult(target, eyes, targetBox, rot); - lastAimPoint = rot.vec(); - lastAimEntityId = target.getId(); - } - - return rot; - } - - private RotationWithVector stabilizeRotationResult(Entity target, Vec3 eyes, AABB targetBox, RotationWithVector rotation) { - if (target == null || eyes == null || targetBox == null || rotation == null) { - return rotation; - } - - Vec3 stablePoint = rotation.vec(); - if (lastAimEntityId == target.getId() && lastAimPoint != null) { - Vec3 previousPoint = clampToBox(lastAimPoint, targetBox); - double distance = previousPoint.distanceTo(stablePoint); - double blend = distance > 0.50 ? 0.55 : distance > 0.20 ? 0.35 : 0.20; - stablePoint = new Vec3( - Mth.lerp(blend, previousPoint.x, stablePoint.x), - Mth.lerp(blend, previousPoint.y, stablePoint.y), - Mth.lerp(blend, previousPoint.z, stablePoint.z) - ); - stablePoint = clampToBox(stablePoint, targetBox); - } - - Rotation stableRotation = Rotation.lookingAt(stablePoint, eyes).normalize(); - return new RotationWithVector(stableRotation, stablePoint); - } - - private AABB predictBox(Entity entity) { - int ticks = Math.max(0, predictTicks.get()); - if (ticks <= 0) { - return entity.getBoundingBox(); - } - - return entity.getBoundingBox().move(entity.getDeltaMovement().scale(ticks)); - } - - private double predictedDistanceSq(Entity entity) { - Minecraft mc = Minecraft.getInstance(); - return TargetingUtil.distanceToBoxSq(mc.player.getEyePosition(), predictBox(entity)); - } - - private boolean shouldAttack(Entity entity) { - Minecraft mc = Minecraft.getInstance(); - Vec3 velocity = entity.getDeltaMovement(); - if (velocity.lengthSqr() < 1.0E-7) { - return false; - } - - Vec3 entityPos = entity.position(); - Vec3 vecToPlayer = mc.player.getBoundingBox().getCenter().subtract(entityPos); - double dot = vecToPlayer.dot(velocity); - boolean movingTowardsPlayer = dot > 0.5 * velocity.length() * vecToPlayer.length(); - - AABB extendedHitbox = mc.player.getBoundingBox().inflate(entity.getBoundingBox().getXsize() * 0.5); - boolean touchesHitbox = extendedHitbox.clip(entityPos, entityPos.add(velocity.scale(20.0))).isPresent(); - boolean willHitPlayer = !extendedHitbox.contains(entityPos) && touchesHitbox; - - if (!movingTowardsPlayer && !willHitPlayer) { - return false; - } - - return predictedDistanceSq(entity) <= range.get() * range.get(); - } - - private boolean isSupportedProjectile(Entity entity) { - if (entity == null || entity.isRemoved()) return false; - EntityType type = entity.getType(); - if (type == net.minecraft.world.entity.EntityTypes.FIREBALL) { - return projectiles.get(PROJECTILE_FIREBALL); - } - if (type == net.minecraft.world.entity.EntityTypes.SHULKER_BULLET) { - return projectiles.get(PROJECTILE_SHULKER_BULLET); - } - return false; - } - - private RotationTarget buildRotationTarget(Rotation rotation, Entity target) { - List processors = new ArrayList<>(); - AngleSmooth smooth = selectAngleSmooth(); - if (smooth != null) { - processors.add(smooth); - } - - return new RotationTarget( - rotation, - target, - processors, - rotationResetTicks.get(), - rotationResetThreshold.get(), - !ignoreOpenInventory.get(), - movementCorrection.get(), - null - ); - } - - private int calculateTicks(Rotation rotation) { - AngleSmooth smooth = selectAngleSmooth(); - if (smooth == null) return 0; - Rotation base = RotationManager.INSTANCE.getServerRotation(); - if (base == null) return 0; - return smooth.calculateTicks(base, rotation); - } - - private AngleSmooth selectAngleSmooth() { - return switch (rotationMode.get()) { - case LINEAR -> linearSmooth; - case SIGMOID -> sigmoidSmooth; - case SMART -> smartSmooth; - case SPOOKY -> spookySmooth; - case NONE -> null; - }; - } - - private void clearTarget() { - currentTarget = null; - lastAttackExecutionAge = -1; - clearRotationState(true); - } - - private void clearTargetImmediate() { - currentTarget = null; - lastAttackExecutionAge = -1; - clearRotationState(false); - } - - private void clearRotationState(boolean smoothReturn) { - if (smoothReturn && rotationReleased) { - return; - } - - RotationManager.INSTANCE.release(this, smoothReturn); - lastAimPoint = null; - lastAimEntityId = -1; - lastRotation = null; - lastRotationTick = -1; - rotationReleased = true; - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/WorldParticles.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/WorldParticles.java deleted file mode 100644 index ba8e7de..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/WorldParticles.java +++ /dev/null @@ -1,821 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.features.module.modules.visuals.effects.ambient; - -import dev.loki.lovisual.util.time.GameClock; - -import dev.loki.lovisual.config.values.color.RGBAColorValue; -import dev.loki.lovisual.config.values.color.RGBColorValue; -import dev.loki.lovisual.config.values.mode.EnumValue; -import dev.loki.lovisual.config.values.mode.ModeValue; -import dev.loki.lovisual.config.values.primitive.BooleanValue; -import dev.loki.lovisual.config.values.primitive.NumberValue; -import dev.loki.lovisual.features.theme.Theme; -import net.minecraft.client.Minecraft; -import net.minecraft.resources.Identifier; -import net.minecraft.util.Mth; -import net.minecraft.world.phys.Vec3; -import org.joml.Quaternionf; -import org.joml.Vector3f; -import dev.loki.lovisual.config.common.CommonSettingSchemas; -import dev.loki.lovisual.features.module.core.Module; -import dev.loki.lovisual.features.module.core.ModuleCategory; -import dev.loki.lovisual.features.module.core.ModuleInfo; -import dev.loki.lovisual.features.module.modules.visuals.optimization.FpsOptimizer; -import dev.loki.lovisual.features.module.phase.WorldPhase; -import dev.loki.lovisual.render.engine.RenderState; -import dev.loki.lovisual.render.engine.texture.TextureStorage; -import dev.loki.lovisual.render.engine.animation.AnimationUtility; -import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors; -import dev.loki.lovisual.render.engine.color.ColorBlendMode; -import dev.loki.lovisual.render.engine.color.ColorEffects; -import dev.loki.lovisual.render.engine.color.RenderColor; -import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; -import dev.loki.lovisual.render.engine.profiler.frame.RenderCostProfiler; -import dev.loki.lovisual.render.engine.renderer.Renderer3D; -import dev.loki.lovisual.features.module.modules.visuals.effects.ambient.geometry.WorldParticlesGeometry; -import dev.loki.lovisual.features.module.modules.visuals.effects.ambient.geometry.WorldParticlesSphere; -import dev.loki.lovisual.render.engine.uniform.MeshBuilder; -import dev.loki.lovisual.render.engine.uniform.impl.hand.effects.TextureTintUniforms; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ThreadLocalRandom; - -//todo Description -@ModuleInfo( - id = "worldparticles", - displayName = "WorldParticles", - category = ModuleCategory.VISUALS -) -public class WorldParticles extends Module { - private static final float SPRITE_SIZE_MULTIPLIER = 4.0f; - private static final float SPRITE_ALPHA_MULTIPLIER = 0.40f; - private static final float DIAGONAL_ALPHA_MULTIPLIER = 0.40f; - private static final float OUTLINE_ALPHA_MULTIPLIER = 205.0f / 255.0f; - private static final float BUBBLE_FILL_SIZE_MULTIPLIER = 3.15f; - private static final float BUBBLE_GLOW_SIZE_MULTIPLIER = 5.10f; - private static final float BUBBLE_HIGHLIGHT_SIZE_MULTIPLIER = 0.34f; - private static final float BUBBLE_SECONDARY_HIGHLIGHT_SIZE_MULTIPLIER = 0.20f; - private static final float BUBBLE_FILL_ALPHA_MULTIPLIER = 0.34f; - private static final float BUBBLE_GLOW_ALPHA_MULTIPLIER = 0.13f; - private static final float BUBBLE_DEPTH_ALPHA_MULTIPLIER = 0.22f; - private static final float BUBBLE_HIGHLIGHT_ALPHA_MULTIPLIER = 0.74f; - private static final int BUBBLE_SPHERE_STACKS = 5; - private static final int BUBBLE_SPHERE_SLICES = 12; - private static final float FUNNEL_EYE_SIZE_MULTIPLIER = 4.35f; - private static final float FUNNEL_DISTORTION_SIZE_MULTIPLIER = 5.25f; - private static final float FUNNEL_DISTORTION_OUTER_SIZE_MULTIPLIER = 6.85f; - private static final float FUNNEL_DISTORTION_INNER_SIZE_MULTIPLIER = 3.95f; - private static final float FUNNEL_EYE_ALPHA_MULTIPLIER = 0.72f; - private static final float FUNNEL_DISTORTION_ALPHA_MULTIPLIER = 0.16f; - private static final float FUNNEL_DISTORTION_OUTER_ALPHA_MULTIPLIER = 0.085f; - private static final float FUNNEL_DISTORTION_INNER_ALPHA_MULTIPLIER = 0.115f; - private static final float FUNNEL_RIM_ALPHA_MULTIPLIER = 0.22f; - private static final float FUNNEL_GHOST_ALPHA_MULTIPLIER = 0.18f; - private static final float FUNNEL_FLASH_ALPHA_MULTIPLIER = 0.34f; - private static final float FUNNEL_LEAK_ALPHA_MULTIPLIER = 0.32f; - private static final float MOTION_SCALE = 0.04f; - private static final float DRAG = 0.98f; - private static final long ALPHA_ANIM_MS = 1000L; - private static final int[][] CUBE_EDGES = { - {0, 1}, {1, 2}, {2, 3}, {3, 0}, - {4, 5}, {5, 6}, {6, 7}, {7, 4}, - {0, 4}, {1, 5}, {2, 6}, {3, 7} - }; - private static final int[][] BODY_DIAGONALS = { - {0, 6}, {1, 7}, {2, 4}, {3, 5} - }; - - private final EnumValue mode = - enumSetting("mode", "mode", Mode.QUADS); - private final BooleanValue depthTest = - boolCommon("depth_test", CommonSettingSchemas.RENDER_DEPTH_TEST, true); - private final RGBAColorValue color = - common(color("color", "#FF8ED4FF"), CommonSettingSchemas.RENDER_PRIMARY_COLOR.commonI18nKey()); - private final ModeValue colorMode = - modeSetting("worldParticlesColorMode", "color_mode", "Static", - "Static", "Rainbow", "LightRainbow", "Sky", "Fade", "DoubleColor", "Analogous", "Theme"); - private final NumberValue colorSpeed = - num("worldParticlesColorSpeed", "color_speed", 18, 2, 54); - private final NumberValue colorPhase = - num("worldParticlesColorPhase", "color_phase", 0, 0, 360); - private final NumberValue colorSpread = - num("worldParticlesColorSpread", "color_spread", 90, -720, 720); - private final RGBAColorValue color2 = - visibleWhen(color("worldParticlesColor2", "color2", "#FF55FFFF"), this::usesSecondaryColor); - private final NumberValue maxParticles = - num("max_particles", 100, 1, 400); - private final NumberValue spawnPerTick = - num("spawn_per_tick", 1, 1, 20); - private final NumberValue radius = - num("radius", 20.0, 1.0, 64.0); - private final NumberValue height = - num("height", 5.0, 0.5, 16.0); - private final NumberValue lifeMinMs = - num("life_min_ms", 1500, 100, 10000); - private final NumberValue lifeMaxMs = - num("life_max_ms", 4500, 100, 15000); - private final NumberValue sizeMin = - num("size_min", 0.10, 0.02, 1.0); - private final NumberValue sizeMax = - num("size_max", 0.30, 0.02, 1.5); - private final NumberValue lineWidth = - visibleWhen(num("line_width", 1.0, 0.5, 4.0), () -> !isFunnelMode() && !isBubbleMode()); - private final EnumValue funnelTintMode = - visibleWhen(enumSetting("funnel_tint_mode", "funnel_tint_mode", ColorBlendMode.ORIGINAL), this::isFunnelMode); - private final BooleanValue funnelTintSyncTheme = - visibleWhen(bool("funnel_tint_sync_theme", "funnel_tint_sync_theme", true), this::isFunnelTintControlsVisible); - private final RGBColorValue funnelTintColor = - visibleWhen(colorNoAlpha("funnel_tint_color", "funnel_tint_color", "#78D7FF"), this::isManualFunnelTintVisible); - private final NumberValue funnelTintStrength = - visibleWhen(num("funnel_tint_strength", "funnel_tint_strength", 0.45f, 0.0f, 1.0f), this::isFunnelTintControlsVisible); - private final List particles = new ArrayList<>(); - - private static double randomDoubleClosed(ThreadLocalRandom random, double min, double max) { - if (!Double.isFinite(min) || !Double.isFinite(max)) { - return 0.02; - } - double low = Math.min(min, max); - double high = Math.max(min, max); - if (high <= low) { - return low; - } - return random.nextDouble(low, high); - } - - private static long randomLongClosed(ThreadLocalRandom random, long min, long max) { - long low = Math.min(min, max); - long high = Math.max(min, max); - if (high <= low) { - return low; - } - if (high == Long.MAX_VALUE) { - return random.nextLong(low, Long.MAX_VALUE) + 1L; - } - return random.nextLong(low, high + 1L); - } - - private static Vec3 offsetInBillboardPlane(Vec3 pos, Quaternionf camRot, float angle, float distance) { - Vector3f right = new Vector3f(1.0f, 0.0f, 0.0f).rotate(camRot).mul((float) Math.cos(angle) * distance); - Vector3f up = new Vector3f(0.0f, 1.0f, 0.0f).rotate(camRot).mul((float) Math.sin(angle) * distance); - return pos.add(right.x() + up.x(), right.y() + up.y(), right.z() + up.z()); - } - - private static int multiplyAlpha(int argb, float alphaMultiplier) { - int alpha = (argb >>> 24) & 0xFF; - int outAlpha = Mth.clamp(Math.round(alpha * alphaMultiplier), 0, 255); - return (outAlpha << 24) | (argb & 0x00FFFFFF); - } - - @Override - public WorldPhase getWorldPhase() { - return WorldPhase.END_MAIN; - } - - @Override - public void onDisable() { - particles.clear(); - } - - @Override - public void onTick() { - Minecraft mc = Minecraft.getInstance(); - if (!isEnabled() || mc.player == null || mc.level == null) { - return; - } - - particles.removeIf(Particle::isExpired); - for (Particle particle : particles) { - particle.tick(); - } - - int targetCount = Math.max(1, maxParticles.get()); - int available = targetCount - particles.size(); - if (available <= 0) { - return; - } - - int spawnCount = Mth.clamp(spawnPerTick.get(), 0, available); - for (int i = 0; i < spawnCount; i++) { - Particle particle = createParticle(); - if (particle != null) { - particles.add(particle); - } - } - } - - @Override - public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { - Minecraft mc = Minecraft.getInstance(); - if (!isEnabled() || particles.isEmpty() || mc.player == null || mc.level == null) { - return; - } - - try (RenderCostProfiler.Scope ignored = RenderCostProfiler.worldEffect("world_particles:" + mode.get())) { - boolean useDepth = depthTest.get(); - Renderer3D.DepthMode depthMode = useDepth ? Renderer3D.DepthMode.PRE_DEPTH : Renderer3D.DepthMode.MAIN; - if (isFunnelMode()) { - renderFunnel(renderer, useDepth, depthMode, tickDelta); - return; - } - - boolean bubbleMode = isBubbleMode(); - MeshBuilder spriteMesh = renderer.batchTextured( - useDepth ? LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE_LIQUID_IGNORE - : LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE, - TextureStorage.BLOOM, - depthMode - ); - - MeshBuilder lineMesh = null; - if (!bubbleMode) { - float prevLineWidth = RenderState.lineWidth; - try { - RenderState.lineWidth = Math.max(0.5f, lineWidth.get().floatValue()); - lineMesh = renderer.batch( - useDepth ? LoVisualRenderPipelines.WORLD_COLORED_LINES_LIQUID_IGNORE - : LoVisualRenderPipelines.WORLD_COLORED_LINES, - depthMode - ); - } finally { - RenderState.lineWidth = prevLineWidth; - } - } - - Quaternionf camRot = RenderState.cameraRotation; - MeshBuilder bubbleShellMesh = bubbleMode - ? renderer.batch( - useDepth ? LoVisualRenderPipelines.WORLD_COLORED_LIQUID_IGNORE - : LoVisualRenderPipelines.WORLD_COLORED, - depthMode - ) - : null; - - int particleCount = particles.size(); - if (bubbleShellMesh != null) { - bubbleShellMesh.ensureCapacity( - particleCount * (BUBBLE_SPHERE_STACKS + 1) * (BUBBLE_SPHERE_SLICES + 1), - particleCount * BUBBLE_SPHERE_STACKS * BUBBLE_SPHERE_SLICES * 6 - ); - } - if (spriteMesh != null) { - spriteMesh.ensureCapacity( - bubbleMode ? particleCount * 20 : particleCount * 4, - bubbleMode ? particleCount * 30 : particleCount * 6 - ); - } - if (lineMesh != null) { - lineMesh.ensureCapacity( - particleCount * 32, - particleCount * 32 - ); - } - - for (int i = 0; i < particles.size(); i++) { - Particle particle = particles.get(i); - float alpha = particle.alpha(); - if (alpha <= 0.003f) { - continue; - } - - Vec3 pos = particle.interpolatePos(tickDelta); - int baseColor = colorForParticle(particle, i); - - if (bubbleMode) { - renderAirBubble(bubbleShellMesh, spriteMesh, particle, pos, camRot, alpha, baseColor); - continue; - } - - if (spriteMesh != null) { - int spriteArgb = multiplyAlpha(baseColor, alpha * SPRITE_ALPHA_MULTIPLIER); - WorldParticlesGeometry.addBillboardQuad(spriteMesh, pos.x, pos.y, pos.z, particle.size * SPRITE_SIZE_MULTIPLIER, camRot, spriteArgb); - } - - if (lineMesh != null) { - Vec3 rotation = particle.interpolateRotation(tickDelta); - int diagonalArgb = multiplyAlpha(baseColor, alpha * DIAGONAL_ALPHA_MULTIPLIER); - int outlineArgb = multiplyAlpha(baseColor, alpha * OUTLINE_ALPHA_MULTIPLIER); - WorldParticlesGeometry.addCube(lineMesh, pos, rotation, particle.size, diagonalArgb, outlineArgb); - } - } - } - } - - private void renderFunnel(Renderer3D renderer, boolean useDepth, Renderer3D.DepthMode depthMode, float tickDelta) { - ColorBlendMode tintMode = funnelTintMode.get(); - TextureTintUniforms.update( - funnelTintRgb(), - tintMode == ColorBlendMode.ORIGINAL ? 0.0f : Mth.clamp(funnelTintStrength.get(), 0.0f, 1.0f), - tintMode.ordinal() - ); - - MeshBuilder distortionMesh = tintedTexturedBatch(renderer, useDepth, depthMode, TextureStorage.FUNNEL_DISTORTION); - MeshBuilder eyeMesh = tintedTexturedBatch(renderer, useDepth, depthMode, TextureStorage.FUNNEL_EYE); - MeshBuilder bloomMesh = FpsOptimizer.bloomDisabled() - ? null - : tintedTexturedBatch(renderer, useDepth, depthMode, TextureStorage.BLOOM); - if (distortionMesh == null && eyeMesh == null && bloomMesh == null) { - return; - } - - int particleCount = particles.size(); - if (distortionMesh != null) { - distortionMesh.ensureCapacity(particleCount * 12, particleCount * 18); - } - if (eyeMesh != null) { - eyeMesh.ensureCapacity(particleCount * 16, particleCount * 24); - } - if (bloomMesh != null) { - bloomMesh.ensureCapacity(particleCount * 20, particleCount * 30); - } - - Quaternionf camRot = RenderState.cameraRotation; - for (int i = 0; i < particles.size(); i++) { - Particle particle = particles.get(i); - float alpha = particle.funnelAlpha(); - if (alpha <= 0.003f) { - continue; - } - - Vec3 pos = particle.interpolatePos(tickDelta); - int baseColor = colorForParticle(particle, i); - float ageSeconds = particle.ageMs() / 1000.0f; - float scale = particle.funnelScale(); - float pulse = 0.5f + 0.5f * (float) Math.sin(particle.ageMs() * 0.0048f + particle.visualPhase); - float rimPulse = AnimationUtility.smoothstep(pulse); - - if (distortionMesh != null) { - float outerRoll = particle.visualPhase * 0.63f - ageSeconds * particle.distortionSpin * 0.54f; - int outerArgb = funnelColor(baseColor, 0xFFFFFF, alpha * FUNNEL_DISTORTION_OUTER_ALPHA_MULTIPLIER * (0.82f + rimPulse * 0.18f)); - WorldParticlesGeometry.addBillboardQuad( - distortionMesh, - pos.x, pos.y, pos.z, - particle.size * FUNNEL_DISTORTION_OUTER_SIZE_MULTIPLIER * scale * (1.0f + rimPulse * 0.055f), - camRot, - outerRoll, - outerArgb - ); - - float distortionRoll = particle.visualPhase + ageSeconds * particle.distortionSpin; - int distortionArgb = funnelColor(baseColor, 0xFFFFFF, alpha * FUNNEL_DISTORTION_ALPHA_MULTIPLIER); - WorldParticlesGeometry.addBillboardQuad( - distortionMesh, - pos.x, pos.y, pos.z, - particle.size * FUNNEL_DISTORTION_SIZE_MULTIPLIER * scale, - camRot, - distortionRoll, - distortionArgb - ); - - float innerRoll = -particle.visualPhase * 1.18f + ageSeconds * (0.18f - particle.distortionSpin * 0.35f); - int innerArgb = funnelColor(baseColor, 0xFFFFFF, alpha * FUNNEL_DISTORTION_INNER_ALPHA_MULTIPLIER * (1.0f - rimPulse * 0.25f)); - WorldParticlesGeometry.addBillboardQuad( - distortionMesh, - pos.x, pos.y, pos.z, - particle.size * FUNNEL_DISTORTION_INNER_SIZE_MULTIPLIER * scale, - camRot, - innerRoll, - innerArgb - ); - } - - if (eyeMesh != null) { - float eyeRoll = -particle.visualPhase * 0.42f + ageSeconds * particle.eyeSpin; - int rimArgb = funnelColor(baseColor, 0xFFC76A, alpha * FUNNEL_RIM_ALPHA_MULTIPLIER * (0.62f + rimPulse * 0.38f)); - WorldParticlesGeometry.addBillboardQuad( - eyeMesh, - pos.x, pos.y, pos.z, - particle.size * (FUNNEL_EYE_SIZE_MULTIPLIER + 0.48f) * scale * (1.0f + rimPulse * 0.045f), - camRot, - eyeRoll - 0.18f, - rimArgb - ); - - int ghostA = funnelColor(baseColor, 0x92E8FF, alpha * FUNNEL_GHOST_ALPHA_MULTIPLIER * (0.70f + rimPulse * 0.16f)); - WorldParticlesGeometry.addBillboardQuad( - eyeMesh, - pos.x, pos.y, pos.z, - particle.size * 3.74f * scale, - camRot, - eyeRoll + 0.74f, - ghostA - ); - - int ghostB = funnelColor(baseColor, 0xFF9D47, alpha * FUNNEL_GHOST_ALPHA_MULTIPLIER * 0.68f * (1.0f - rimPulse * 0.18f)); - WorldParticlesGeometry.addBillboardQuad( - eyeMesh, - pos.x, pos.y, pos.z, - particle.size * 4.88f * scale, - camRot, - eyeRoll - 1.06f, - ghostB - ); - - int eyeArgb = funnelColor(baseColor, 0xFFFFFF, alpha * FUNNEL_EYE_ALPHA_MULTIPLIER); - WorldParticlesGeometry.addBillboardQuad( - eyeMesh, - pos.x, pos.y, pos.z, - particle.size * FUNNEL_EYE_SIZE_MULTIPLIER * scale, - camRot, - eyeRoll, - eyeArgb - ); - } - - if (bloomMesh != null) { - renderFunnelLeaks(bloomMesh, particle, pos, camRot, alpha, scale, baseColor, ageSeconds); - - float lifeT = particle.lifeProgress(); - float flashCenter = 0.34f + ColorEffects.hash01(particle.visualPhase + 5.73f) * 0.38f; - float flash = 1.0f - AnimationUtility.smoothstep(Mth.clamp(Math.abs(lifeT - flashCenter) / 0.13f, 0.0f, 1.0f)); - if (flash > 0.002f) { - int flashArgb = funnelColor(baseColor, 0x7FE8FF, alpha * FUNNEL_FLASH_ALPHA_MULTIPLIER * flash); - WorldParticlesGeometry.addBillboardQuad( - bloomMesh, - pos.x, pos.y, pos.z, - particle.size * (1.45f + flash * 0.82f) * scale, - camRot, - particle.visualPhase * 0.21f, - flashArgb - ); - } - } - } - } - - private void renderAirBubble(MeshBuilder shellMesh, - MeshBuilder spriteMesh, - Particle particle, - Vec3 pos, - Quaternionf camRot, - float alpha, - int baseColor) { - float ageSeconds = particle.ageMs() / 1000.0f; - float lifeT = particle.lifeProgress(); - float pulse = 0.5f + 0.5f * (float) Math.sin(ageSeconds * 3.1f + particle.visualPhase); - float pop = 1.0f + AnimationUtility.smoothstep(pulse) * 0.09f; - float radius = particle.size * BUBBLE_FILL_SIZE_MULTIPLIER * pop; - float lightAngle = particle.visualPhase + 2.35f + ageSeconds * 0.16f; - float depthAngle = lightAngle + (float) Math.PI; - int brightColor = AnimatedRenderColors.mixArgb(baseColor, 0xFFFFFFFF, 0.72f); - int softColor = AnimatedRenderColors.mixArgb(baseColor, 0xFFFFFFFF, 0.32f); - int shadowColor = AnimatedRenderColors.mixArgb(baseColor, 0xFF000000, 0.42f); - Quaternionf sphereRotation = new Quaternionf() - .rotateY(particle.visualPhase + ageSeconds * 0.34f) - .rotateX(particle.visualPhase * 0.47f + ageSeconds * 0.22f) - .rotateZ(particle.visualPhase * -0.31f + ageSeconds * 0.16f); - float sphereRadius = radius * 0.82f; - - if (shellMesh != null) { - WorldParticlesSphere.addSphereShell(shellMesh, pos, sphereRotation, sphereRadius, baseColor, brightColor, alpha); - } - - if (spriteMesh != null) { - WorldParticlesGeometry.addBillboardQuadGradient( - spriteMesh, - pos.x, pos.y, pos.z, - particle.size * BUBBLE_GLOW_SIZE_MULTIPLIER * pop, - camRot, - particle.visualPhase * 0.22f + ageSeconds * 0.12f, - multiplyAlpha(shadowColor, alpha * BUBBLE_GLOW_ALPHA_MULTIPLIER * 0.58f), - multiplyAlpha(baseColor, alpha * BUBBLE_GLOW_ALPHA_MULTIPLIER * 0.88f), - multiplyAlpha(brightColor, alpha * BUBBLE_GLOW_ALPHA_MULTIPLIER * 1.32f), - multiplyAlpha(softColor, alpha * BUBBLE_GLOW_ALPHA_MULTIPLIER) - ); - - Vec3 depth = offsetInBillboardPlane(pos, camRot, depthAngle, radius * 0.22f); - float depthAlpha = alpha * BUBBLE_DEPTH_ALPHA_MULTIPLIER * (0.78f + pulse * 0.18f); - WorldParticlesGeometry.addBillboardQuadGradient( - spriteMesh, - depth.x, depth.y, depth.z, - radius * 0.58f, - camRot, - particle.visualPhase * 0.31f, - multiplyAlpha(shadowColor, depthAlpha * 0.92f), - multiplyAlpha(baseColor, depthAlpha * 0.76f), - multiplyAlpha(softColor, depthAlpha * 0.42f), - multiplyAlpha(shadowColor, depthAlpha * 0.58f) - ); - - float fillAlpha = alpha * BUBBLE_FILL_ALPHA_MULTIPLIER; - WorldParticlesGeometry.addBillboardQuadGradient( - spriteMesh, - pos.x, pos.y, pos.z, - radius, - camRot, - particle.visualPhase * -0.16f, - multiplyAlpha(shadowColor, fillAlpha * 0.92f), - multiplyAlpha(baseColor, fillAlpha * 1.06f), - multiplyAlpha(brightColor, fillAlpha * 0.98f), - multiplyAlpha(softColor, fillAlpha * 1.18f) - ); - - Vec3 highlight = offsetInBillboardPlane(pos, camRot, lightAngle, radius * 0.42f); - float highlightAlpha = alpha * BUBBLE_HIGHLIGHT_ALPHA_MULTIPLIER * (1.0f - lifeT * 0.22f); - WorldParticlesGeometry.addBillboardQuadGradient( - spriteMesh, - highlight.x, highlight.y, highlight.z, - particle.size * BUBBLE_HIGHLIGHT_SIZE_MULTIPLIER * (0.74f + pulse * 0.22f), - camRot, - particle.visualPhase, - multiplyAlpha(softColor, highlightAlpha * 0.46f), - multiplyAlpha(brightColor, highlightAlpha * 0.72f), - multiplyAlpha(0xFFFFFFFF, highlightAlpha), - multiplyAlpha(brightColor, highlightAlpha * 0.86f) - ); - - Vec3 secondaryHighlight = offsetInBillboardPlane(pos, camRot, lightAngle - 0.92f, radius * 0.18f); - float secondaryHighlightAlpha = alpha * BUBBLE_HIGHLIGHT_ALPHA_MULTIPLIER * 0.42f * (0.84f + pulse * 0.16f); - WorldParticlesGeometry.addBillboardQuadGradient( - spriteMesh, - secondaryHighlight.x, secondaryHighlight.y, secondaryHighlight.z, - particle.size * BUBBLE_SECONDARY_HIGHLIGHT_SIZE_MULTIPLIER, - camRot, - particle.visualPhase * -0.38f, - multiplyAlpha(baseColor, secondaryHighlightAlpha * 0.44f), - multiplyAlpha(brightColor, secondaryHighlightAlpha * 0.68f), - multiplyAlpha(0xFFFFFFFF, secondaryHighlightAlpha * 0.82f), - multiplyAlpha(softColor, secondaryHighlightAlpha * 0.58f) - ); - } - - } - - private MeshBuilder tintedTexturedBatch(Renderer3D renderer, - boolean useDepth, - Renderer3D.DepthMode depthMode, - Identifier texture) { - return renderer.batch( - useDepth - ? LoVisualRenderPipelines.WORLD_TEXTURED_TINT_ADDITIVE_LIQUID_IGNORE - : LoVisualRenderPipelines.WORLD_TEXTURED_TINT_ADDITIVE, - depthMode, - Renderer3D.BatchBindings.none() - .withSampler("u_Texture", texture) - .withUniform("TextureTint", TextureTintUniforms.get()) - ); - } - - private Particle createParticle() { - Minecraft mc = Minecraft.getInstance(); - ThreadLocalRandom random = ThreadLocalRandom.current(); - Vec3 playerPos = mc.player.position(); - - double range = Math.max(0.1, radius.get()); - double heightRange = Math.max(0.1, height.get()); - double px = playerPos.x + random.nextDouble(-range, range); - double py = playerPos.y + random.nextDouble(0.0, heightRange); - double pz = playerPos.z + random.nextDouble(-range, range); - - Vec3 pos = new Vec3(px, py, pz); - boolean bubbleMode = isBubbleMode(); - Vec3 motion = bubbleMode - ? new Vec3( - random.nextDouble(-0.42, 0.42) * MOTION_SCALE, - random.nextDouble(1.10, 2.55) * MOTION_SCALE, - random.nextDouble(-0.42, 0.42) * MOTION_SCALE - ) - : new Vec3( - random.nextDouble(-1.0, 1.0) * MOTION_SCALE, - random.nextDouble(0.0, 2.0) * MOTION_SCALE, - random.nextDouble(-1.0, 1.0) * MOTION_SCALE - ); - Vec3 rotationMotion = new Vec3( - random.nextDouble(-1.0, 1.0) * MOTION_SCALE, - random.nextDouble(-1.0, 1.0) * MOTION_SCALE, - random.nextDouble(-1.0, 1.0) * MOTION_SCALE - ); - - int minLife = Math.max(1, Math.min(lifeMinMs.get(), lifeMaxMs.get())); - int maxLife = Math.max(minLife, Math.max(lifeMinMs.get(), lifeMaxMs.get())); - long life = randomLongClosed(random, minLife, maxLife); - - double minSize = Math.max(0.02, Math.min(sizeMin.get(), sizeMax.get())); - double maxSize = Math.max(minSize, Math.max(sizeMin.get(), sizeMax.get())); - float size = (float) randomDoubleClosed(random, minSize, maxSize); - - float visualPhase = (float) random.nextDouble(0.0, Math.PI * 2.0); - float eyeSpin = (float) random.nextDouble(0.38, 0.72); - float distortionSpin = (float) random.nextDouble(-0.28, -0.12); - - return new Particle(pos, motion, rotationMotion, life, size, visualPhase, eyeSpin, distortionSpin); - } - - private void renderFunnelLeaks(MeshBuilder mesh, - Particle particle, - Vec3 pos, - Quaternionf camRot, - float alpha, - float scale, - int baseColor, - float ageSeconds) { - for (int i = 0; i < 4; i++) { - float seed = ColorEffects.hash01(particle.visualPhase + i * 13.17f); - float angle = particle.visualPhase + i * 1.5708f + ageSeconds * (0.28f + seed * 0.16f); - float distance = particle.size * scale * (1.45f + seed * 1.10f); - Vec3 leakPos = offsetInBillboardPlane(pos, camRot, angle, distance); - - float wave = 0.5f + 0.5f * (float) Math.sin(ageSeconds * (2.0f + seed * 0.9f) + particle.visualPhase + i); - float leakAlpha = alpha * FUNNEL_LEAK_ALPHA_MULTIPLIER * (0.45f + AnimationUtility.smoothstep(wave) * 0.55f); - int leakRgb = switch (i & 3) { - case 0 -> 0xFF3A24; - case 1 -> 0xA7FF2E; - case 2 -> 0xFFD84E; - default -> 0x55F7FF; - }; - - WorldParticlesGeometry.addBillboardRect( - mesh, - leakPos.x, leakPos.y, leakPos.z, - particle.size * scale * (0.055f + seed * 0.035f), - particle.size * scale * (0.42f + seed * 0.34f), - camRot, - angle - 1.5708f, - funnelColor(baseColor, leakRgb, leakAlpha) - ); - } - } - - private int funnelColor(int baseArgb, int sourceRgb, float alphaMultiplier) { - return ColorEffects.argbWithBaseAlpha(baseArgb, sourceRgb, alphaMultiplier); - } - - private int colorForParticle(Particle particle, int index) { - int phase = colorPhase.get() - + Math.round(index * colorSpread.get() / 90.0f) - + Math.round(particle.visualPhase * 57.29578f); - return AnimatedRenderColors.resolve( - animatedColorMode(), - colorSpeed.get(), - phase, - color.getArgb(), - color2.getArgb(), - true - ); - } - - private boolean usesSecondaryColor() { - return AnimatedRenderColors.usesSecondary(animatedColorMode()); - } - - private AnimatedRenderColors.Mode animatedColorMode() { - return switch (colorMode.get()) { - case "Rainbow" -> AnimatedRenderColors.Mode.RAINBOW; - case "LightRainbow" -> AnimatedRenderColors.Mode.LIGHT_RAINBOW; - case "Sky" -> AnimatedRenderColors.Mode.SKY; - case "Fade" -> AnimatedRenderColors.Mode.FADE; - case "DoubleColor" -> AnimatedRenderColors.Mode.DOUBLE_COLOR; - case "Analogous" -> AnimatedRenderColors.Mode.ANALOGOUS; - case "Theme" -> AnimatedRenderColors.Mode.THEME; - default -> AnimatedRenderColors.Mode.STATIC; - }; - } - - private int funnelTintRgb() { - if (funnelTintSyncTheme.get()) { - return Theme.theme().accent() & 0x00FFFFFF; - } - return funnelTintColor.getArgb() & 0x00FFFFFF; - } - - private boolean isFunnelTintControlsVisible() { - return isFunnelMode() && funnelTintMode.get() != ColorBlendMode.ORIGINAL; - } - - private boolean isManualFunnelTintVisible() { - return isFunnelTintControlsVisible() && !funnelTintSyncTheme.get(); - } - - private boolean isFunnelMode() { - return mode.get() == Mode.FUNNEL; - } - - private boolean isBubbleMode() { - return mode.get() == Mode.AIR_BUBBLES; - } - - private enum Mode implements EnumValue.AliasProvider { - QUADS, - AIR_BUBBLES, - FUNNEL; - - @Override - public List aliases() { - return switch (this) { - case AIR_BUBBLES -> List.of("AirBubbles"); - case QUADS -> List.of("Quads"); - case FUNNEL -> List.of("Funnel"); - }; - } - } - - private static final class Particle { - private final long spawnMs; - private final long lifeMs; - private final float size; - private final float visualPhase; - private final float eyeSpin; - private final float distortionSpin; - private Vec3 prevPos; - private Vec3 pos; - private Vec3 prevRotation; - private Vec3 rotation; - private Vec3 motion; - private Vec3 rotationMotion; - - private Particle(Vec3 pos, Vec3 motion, Vec3 rotationMotion, long lifeMs, float size, - float visualPhase, float eyeSpin, float distortionSpin) { - this.prevPos = pos; - this.pos = pos; - this.prevRotation = Vec3.ZERO; - this.rotation = Vec3.ZERO; - this.motion = motion; - this.rotationMotion = rotationMotion; - this.spawnMs = GameClock.millis(); - this.lifeMs = Math.max(1L, lifeMs); - this.size = size; - this.visualPhase = visualPhase; - this.eyeSpin = eyeSpin; - this.distortionSpin = distortionSpin; - } - - private void tick() { - prevPos = pos; - prevRotation = rotation; - pos = pos.add(motion); - rotation = rotation.add(rotationMotion); - motion = motion.scale(DRAG); - rotationMotion = rotationMotion.scale(DRAG); - } - - private float alpha() { - long age = GameClock.millis() - spawnMs; - if (age <= 0L) { - return 0.0f; - } - if (age < ALPHA_ANIM_MS) { - return AnimationUtility.smoothstep(age / (float) ALPHA_ANIM_MS); - } - if (age < lifeMs) { - return 1.0f; - } - float fade = 1.0f - (age - lifeMs) / (float) ALPHA_ANIM_MS; - return AnimationUtility.smoothstep(Mth.clamp(fade, 0.0f, 1.0f)); - } - - private float funnelAlpha() { - long age = ageMs(); - if (age <= 0L) { - return 0.0f; - } - - float appear = AnimationUtility.easeOutCubic(age / 720.0f); - float disappear = 1.0f; - if (age > lifeMs) { - disappear = 1.0f - AnimationUtility.easeInCubic((age - lifeMs) / (float) ALPHA_ANIM_MS); - } - - float breath = 0.88f + 0.12f * (float) Math.sin(age * 0.0032f + visualPhase); - return AnimationUtility.clamp01(appear * disappear * breath); - } - - private float funnelScale() { - long age = ageMs(); - float appear = AnimationUtility.easeOutCubic(age / 900.0f); - float disappear = 1.0f; - if (age > lifeMs) { - disappear = 1.0f - AnimationUtility.smoothstep((age - lifeMs) / (float) ALPHA_ANIM_MS); - } - - float open = AnimationUtility.lerp(0.68f, 1.04f, appear); - float close = AnimationUtility.lerp(0.82f, 1.0f, disappear); - return open * close; - } - - private long ageMs() { - return GameClock.millis() - spawnMs; - } - - private float lifeProgress() { - return AnimationUtility.clamp01(ageMs() / (float) Math.max(1L, lifeMs)); - } - - private boolean isExpired() { - return GameClock.millis() - spawnMs >= lifeMs + ALPHA_ANIM_MS; - } - - private Vec3 interpolatePos(float tickDelta) { - return prevPos.lerp(pos, tickDelta); - } - - private Vec3 interpolateRotation(float tickDelta) { - return prevRotation.lerp(rotation, tickDelta); - } - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/HitEffect.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/HitEffect.java deleted file mode 100644 index 0394426..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/HitEffect.java +++ /dev/null @@ -1,825 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.features.module.modules.visuals.effects.hits; - -import dev.loki.lovisual.util.time.GameClock; - -import com.mojang.blaze3d.pipeline.RenderPipeline; -import dev.loki.lovisual.config.values.color.RGBAColorValue; -import dev.loki.lovisual.config.values.mode.EnumValue; -import dev.loki.lovisual.config.values.mode.ModeValue; -import dev.loki.lovisual.config.values.primitive.BooleanValue; -import dev.loki.lovisual.config.values.primitive.NumberValue; -import net.minecraft.client.Minecraft; -import net.minecraft.core.BlockPos; -import net.minecraft.resources.Identifier; -import net.minecraft.util.Mth; -import net.minecraft.world.entity.Entity; -import net.minecraft.world.entity.LivingEntity; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.phys.Vec3; -import net.minecraft.world.phys.shapes.VoxelShape; -import org.joml.Quaternionf; -import org.joml.Vector3f; -import dev.loki.lovisual.features.module.core.Module; -import dev.loki.lovisual.features.module.core.ModuleCategory; -import dev.loki.lovisual.features.module.core.ModuleInfo; -import dev.loki.lovisual.features.module.modules.visuals.optimization.FpsOptimizer; -import dev.loki.lovisual.features.module.phase.WorldPhase; -import dev.loki.lovisual.mixininterface.entity.ILocalPlayer; -import dev.loki.lovisual.mixininterface.entity.IPlayerAttackCooldown; -import dev.loki.lovisual.render.engine.RenderState; -import dev.loki.lovisual.render.engine.texture.TextureStorage; -import dev.loki.lovisual.render.engine.animation.AnimatedColorModeOptions; -import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors; -import dev.loki.lovisual.render.engine.color.RenderColor; -import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; -import dev.loki.lovisual.render.engine.renderer.Renderer3D; -import dev.loki.lovisual.render.engine.uniform.MeshBuilder; -import dev.loki.lovisual.render.helpers.particle.OrientedQuadRenderer; -import dev.loki.lovisual.render.helpers.player.Particle3D; -import dev.loki.lovisual.render.helpers.util.ParticleTextureMode; -import dev.loki.lovisual.util.combat.hit.AttackHitPoint; -import dev.loki.lovisual.util.time.Timer; - -import java.awt.*; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Random; - -//todo Description -@ModuleInfo(id = "hiteffect", displayName = "HitEffect", category = ModuleCategory.VISUALS) -public class HitEffect extends Module { - - private static final String SETTING_EFFECT_MODE = "effect_mode"; - private static final String SETTING_LIFE_TIME = "life_time"; - private static final String SETTING_SCALE = "scale"; - private static final String SETTING_ONLY_PLAYERS = "only_players"; - private static final String SETTING_DEPTH_TEST = "depth_test"; - private static final String SETTING_STARS = "stars"; - private static final String SETTING_STARS_COLOR = "stars_color"; - private static final String SETTING_CRIT_STARS_COLOR = "crit_stars_color"; - private static final String SETTING_COLOR_MODE = "color_mode"; - private static final String SETTING_COLOR_SPEED = "color_speed"; - private static final String SETTING_COLOR = "color"; - private static final String SETTING_COLOR2 = "color2"; - private static final String SETTING_WAVE_RADIUS = "wave_radius"; - private static final String SETTING_WAVE_DURATION = "wave_duration"; - private static final String SETTING_IMPACT_RADIUS = "impact_radius"; - private static final String SETTING_IMPACT_DURATION = "impact_duration"; - private static final String SETTING_IMPACT_GLOW_HEIGHT = "impact_glow_height"; - private static final int IMPACT_RING_SEGMENTS = 24; - private static final String SETTING_HIT_PARTICLES = "hit_particles"; - private static final String SETTING_HIT_PARTICLE_COUNT = "hit_particle_count"; - private static final String SETTING_HIT_PARTICLE_SIZE = "hit_particle_size"; - private static final String SETTING_HIT_PARTICLE_LIFE = "hit_particle_life_ms"; - private static final String SETTING_HIT_PARTICLE_TEXTURE = "hit_particle_texture"; - private static final String SETTING_HIT_PARTICLE_RANDOM_COLOR = "hit_particle_random_color"; - private static final int NORMAL_STARS = 12; - private static final int CRIT_STARS = 18; - private static final float CHARGED_THRESHOLD = 0.9f; - private static final float WAVE_WIDTH = 2.5f; - private static final int WAVE_MAX_PER_FRAME = 400; - private static final float WAVE_MIN_ALPHA = 0.02f; - private static final double WAVE_OUTLINE_EPS = 0.002; - private static final float WAVE_LINE_WIDTH_BASE = 0.8f; - private static final float WAVE_LINE_WIDTH_BOOST = 0.6f; - private static final float HIT_PARTICLE_GRAVITY = 0.02f; - private static final float HIT_PARTICLE_DRAG = 0.94f; - private static final int HIT_PARTICLE_MAX = 320; - private static final int HIT_PARTICLE_SPAWN_CAP_PER_TICK = 32; - private final Random random = new Random(); - private final EnumValue effectMode = - enumSetting("hitBubblesEffectMode", SETTING_EFFECT_MODE, EffectMode.BUBBLES, EffectMode.values()); - private final NumberValue lifeTime = - visibleWhen(num("hitBubblesLifeTime", SETTING_LIFE_TIME, 30, 1, 150), this::isBubblesMode); - private final NumberValue bubbleScale = - visibleWhen(num("hitBubblesScale", SETTING_SCALE, 1.0f, 0.4f, 2.0f), this::isBubblesMode); - private final ModeValue colorMode = - modeSetting("hitBubblesColorMode", SETTING_COLOR_MODE, "Static", - "Static", "Rainbow", "LightRainbow", "Sky", "Fade", "DoubleColor", "Analogous", "Theme"); - private final NumberValue colorSpeed = - num("hitBubblesColorSpeed", SETTING_COLOR_SPEED, 18, 2, 54); - private final RGBAColorValue colorValue = color("hitBubblesColor", SETTING_COLOR, "#FF55FFFF"); - private final RGBAColorValue colorValue2 = - visibleWhen(color("hitBubblesColor2", SETTING_COLOR2, "#FFFF5555"), this::usesSecondaryColor); - private final BooleanValue depthTest = - bool("hitBubblesDepthTest", SETTING_DEPTH_TEST, true); - private final BooleanValue onlyPlayers = - bool("hitBubblesOnlyPlayers", SETTING_ONLY_PLAYERS, true); - private final BooleanValue starsEnabled = - visibleWhen(bool("hitBubblesStarsEnabled", SETTING_STARS, true), this::isBubblesMode); - private final RGBAColorValue starsColor = - visibleWhen(color("hitBubblesStarsColor", SETTING_STARS_COLOR, "#FFFFE16B"), - () -> isBubblesMode() && starsEnabled.get()); - private final RGBAColorValue critStarsColor = - visibleWhen(color("hitBubblesCritStarsColor", SETTING_CRIT_STARS_COLOR, "#FFFF3A2E"), - () -> isBubblesMode() && starsEnabled.get()); - private final NumberValue waveRadius = - visibleWhen(num("hitBubblesWaveRadius", SETTING_WAVE_RADIUS, 12, 3, 32), this::isWaveMode); - private final NumberValue waveDurationMs = - visibleWhen(num("hitBubblesWaveDuration", SETTING_WAVE_DURATION, 1500, 200, 6000), this::isWaveMode); - private final NumberValue impactRadius = - visibleWhen(num("hitBubblesImpactRadius", SETTING_IMPACT_RADIUS, 1.4f, 0.4f, 4.0f), this::isImpactMode); - private final NumberValue impactDurationMs = - visibleWhen(num("hitBubblesImpactDuration", SETTING_IMPACT_DURATION, 450, 150, 1500), this::isImpactMode); - private final NumberValue impactGlowHeight = - visibleWhen(num("hitBubblesImpactGlowHeight", SETTING_IMPACT_GLOW_HEIGHT, 0.8f, 0.2f, 2.5f), this::isImpactMode); - private final BooleanValue hitParticlesEnabled = - bool("hitEffectParticlesEnabled", SETTING_HIT_PARTICLES, false); - private final NumberValue hitParticleCount = - visibleWhen(num("hitEffectParticleCount", SETTING_HIT_PARTICLE_COUNT, 15, 1, 80), hitParticlesEnabled::get); - private final NumberValue hitParticleSize = - visibleWhen(num("hitEffectParticleSize", SETTING_HIT_PARTICLE_SIZE, 0.12f, 0.04f, 0.4f), hitParticlesEnabled::get); - private final NumberValue hitParticleLifeMs = - visibleWhen(num("hitEffectParticleLifeMs", SETTING_HIT_PARTICLE_LIFE, 2200, 200, 6000), hitParticlesEnabled::get); - private final EnumValue hitParticleTexture = - visibleWhen(enumSetting("hitEffectParticleTexture", SETTING_HIT_PARTICLE_TEXTURE, ParticleTextureMode.BLOOM, ParticleTextureMode.values()), - hitParticlesEnabled::get); - private final BooleanValue hitParticleRandomColor = - visibleWhen(bool("hitEffectParticleRandomColor", SETTING_HIT_PARTICLE_RANDOM_COLOR, false), hitParticlesEnabled::get); - private final List bubbles = new ArrayList<>(); - private final List stars = new ArrayList<>(); - private final List waves = new ArrayList<>(); - private final List impacts = new ArrayList<>(); - private final List hitParticles = new ArrayList<>(); - private final List[] hitParticleBuckets = createBuckets(); - private int lastHitParticleTick = Integer.MIN_VALUE; - private int spawnedHitParticlesThisTick = 0; - - @SuppressWarnings("unchecked") - private static List[] createBuckets() { - List[] buckets = new List[TextureStorage.RANDOM_PARTICLES.length]; - for (int i = 0; i < buckets.length; i++) { - buckets[i] = new ArrayList<>(); - } - return buckets; - } - - private static void clearBuckets(List[] buckets) { - for (List bucket : buckets) { - bucket.clear(); - } - } - - private static int applyOpacity(int argb, float opacity) { - opacity = Math.min(1f, Math.max(0f, opacity)); - int a = (argb >>> 24) & 0xFF; - int na = Mth.clamp((int) (a * opacity), 0, 255); - return (na << 24) | (argb & 0x00FFFFFF); - } - - private static void addBillboardQuad(MeshBuilder mesh, double cx, double cy, double cz, - float size, Quaternionf camRot, int argb) { - Vector3f right = new Vector3f(1, 0, 0).rotate(camRot).mul(size); - Vector3f up = new Vector3f(0, 1, 0).rotate(camRot).mul(size); - - double p1x = cx - right.x() - up.x(); - double p1y = cy - right.y() - up.y(); - double p1z = cz - right.z() - up.z(); - double p2x = cx + right.x() - up.x(); - double p2y = cy + right.y() - up.y(); - double p2z = cz + right.z() - up.z(); - double p3x = cx + right.x() + up.x(); - double p3y = cy + right.y() + up.y(); - double p3z = cz + right.z() + up.z(); - double p4x = cx - right.x() + up.x(); - double p4y = cy - right.y() + up.y(); - double p4z = cz - right.z() + up.z(); - - mesh.ensureQuadCapacity(); - int i1 = mesh.vec3(p1x, p1y, p1z).vec2(0, 1).color(new RenderColor(argb)).next(); - int i2 = mesh.vec3(p2x, p2y, p2z).vec2(1, 1).color(new RenderColor(argb)).next(); - int i3 = mesh.vec3(p3x, p3y, p3z).vec2(1, 0).color(new RenderColor(argb)).next(); - int i4 = mesh.vec3(p4x, p4y, p4z).vec2(0, 0).color(new RenderColor(argb)).next(); - mesh.quad(i1, i2, i3, i4); - } - - private static void addShapeOutline(MeshBuilder mesh, BlockPos pos, VoxelShape shape, int argb, double eps) { - if (mesh == null || shape == null) return; - - int a = (argb >>> 24) & 0xFF; - int r = (argb >>> 16) & 0xFF; - int g = (argb >>> 8) & 0xFF; - int b = argb & 0xFF; - - VoxelShape offset = shape.move(pos.getX(), pos.getY(), pos.getZ()); - offset.forAllEdges((x1, y1, z1, x2, y2, z2) -> { - double cx = pos.getX() + 0.5; - double cy = pos.getY() + 0.5; - double cz = pos.getZ() + 0.5; - - double sx1 = Math.signum(x1 - cx); - double sy1 = Math.signum(y1 - cy); - double sz1 = Math.signum(z1 - cz); - double sx2 = Math.signum(x2 - cx); - double sy2 = Math.signum(y2 - cy); - double sz2 = Math.signum(z2 - cz); - - double ax1 = x1 + sx1 * eps; - double ay1 = y1 + sy1 * eps; - double az1 = z1 + sz1 * eps; - double ax2 = x2 + sx2 * eps; - double ay2 = y2 + sy2 * eps; - double az2 = z2 + sz2 * eps; - - mesh.ensureLineCapacity(); - int i1 = mesh.vec3(ax1, ay1, az1).color(r, g, b, a).next(); - int i2 = mesh.vec3(ax2, ay2, az2).color(r, g, b, a).next(); - mesh.line(i1, i2); - }); - } - - @Override - public WorldPhase getWorldPhase() { - return WorldPhase.END_MAIN; - } - - @Override - public void onDisable() { - bubbles.clear(); - stars.clear(); - waves.clear(); - impacts.clear(); - hitParticles.clear(); - lastHitParticleTick = Integer.MIN_VALUE; - spawnedHitParticlesThisTick = 0; - } - - public void handleHit(Entity target) { - Minecraft mc = Minecraft.getInstance(); - if (!isEnabled() || mc.player == null || mc.level == null) return; - if (!(target instanceof LivingEntity)) return; - if (onlyPlayers.get() && !(target instanceof Player)) return; - - if (effectMode.get() == EffectMode.WAVE) { - Vec3 pos = target.position(); - BlockPos basePos = BlockPos.containing(pos.x, pos.y - 0.1, pos.z); - addWave(basePos); - } else if (effectMode.get() == EffectMode.IMPACT) { - impacts.add(new ImpactRing(target.position(), GameClock.millis())); - } else { - ILocalPlayer access = (ILocalPlayer) mc.player; - float yaw = access.lovisual$getLastYaw(); - float pitch = access.lovisual$getLastPitch(); - - Vec3 point = AttackHitPoint.resolve(target, yaw, pitch); - if (point == null) point = target.position(); - - bubbles.add(new HitBubble(point, -yaw, pitch, new Timer())); - - if (starsEnabled.get() && mc.player instanceof IPlayerAttackCooldown cooldownAccess) { - float cooldown = cooldownAccess.lovisual$getAttackCooldownProgress(0.0f); - if (cooldown >= CHARGED_THRESHOLD) { - boolean crit = isCritical(mc.player, cooldown); - spawnStars(point, crit); - } - } - } - - if (hitParticlesEnabled.get()) { - spawnHitParticles((LivingEntity) target); - } - } - - @Override - public void onTick() { - Minecraft mc = Minecraft.getInstance(); - if (!isEnabled() || mc.player == null || mc.level == null) return; - - if (effectMode.get() != EffectMode.BUBBLES || !starsEnabled.get()) { - stars.clear(); - } else if (!stars.isEmpty()) { - stars.removeIf(Star::update); - } - - if (!hitParticles.isEmpty()) { - hitParticles.removeIf(Particle3D::update); - } - } - - @Override - public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { - Minecraft mc = Minecraft.getInstance(); - if (!isEnabled() || mc.player == null || mc.level == null) return; - - if (hitParticlesEnabled.get() && !hitParticles.isEmpty()) { - renderHitParticles(renderer, tickDelta); - } - - if (effectMode.get() == EffectMode.WAVE) { - renderWaveEffect(renderer); - return; - } - if (effectMode.get() == EffectMode.IMPACT) { - renderImpactEffect(renderer); - return; - } - - renderBubbleEffect(renderer, tickDelta); - } - - private void renderImpactEffect(Renderer3D renderer) { - if (impacts.isEmpty()) return; - - Iterator iterator = impacts.iterator(); - while (iterator.hasNext()) { - ImpactRing impact = iterator.next(); - if (impact.isExpired()) { - iterator.remove(); - continue; - } - impact.render(renderer); - } - } - - private void renderBubbleEffect(Renderer3D renderer, float tickDelta) { - boolean useDepth = depthTest.get(); - RenderPipeline pipeline = useDepth - ? LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE_LIQUID_IGNORE - : LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE; - Renderer3D.DepthMode depthMode = useDepth ? Renderer3D.DepthMode.PRE_DEPTH : Renderer3D.DepthMode.MAIN; - - if (!bubbles.isEmpty()) { - MeshBuilder mesh = renderer.batchTextured(pipeline, TextureStorage.BUBBLE, depthMode); - if (mesh == null) return; - - for (HitBubble b : bubbles) { - float lifeMs = b.life.getPassedTimeMs(); - float lifeTotal = Math.max(1f, lifeTime.get() * 50f); - float angle = -lifeMs / 4f; - float factor = Math.min(1f, lifeMs / lifeTotal); - float scale = factor * 2f * bubbleScale.get(); - if (scale <= 0.01f) continue; - - int c1 = applyOpacity(getColorArgb(270), 1f - factor); - int c2 = applyOpacity(getColorArgb(0), 1f - factor); - int c3 = applyOpacity(getColorArgb(180), 1f - factor); - int c4 = applyOpacity(getColorArgb(90), 1f - factor); - - OrientedQuadRenderer.addOrientedQuad(mesh, b.pos, b.yaw, b.pitch, angle, scale, c1, c2, c3, c4); - } - - bubbles.removeIf(b -> b.life.passedMs(lifeTime.get() * 50L)); - } - - if (starsEnabled.get() && !stars.isEmpty()) { - MeshBuilder mesh = renderer.batchTextured(pipeline, TextureStorage.PARTICLE_STARS, depthMode); - if (mesh == null) return; - - Quaternionf camRot = RenderState.cameraRotation; - - for (Star s : stars) { - Vec3 pos = s.interpolate(tickDelta); - float t = s.getLifeProgress(); - float scale = s.size * (1f - t); - if (scale <= 0.01f) continue; - - int argb = applyOpacity(s.color, 1f - t); - addBillboardQuad(mesh, pos.x, pos.y, pos.z, scale, camRot, argb); - } - - } - } - - private void renderWaveEffect(Renderer3D renderer) { - if (waves.isEmpty()) return; - - Iterator iterator = waves.iterator(); - while (iterator.hasNext()) { - WaveEffect wave = iterator.next(); - if (wave.isExpired()) { - iterator.remove(); - continue; - } - wave.render(renderer); - } - } - - private void addWave(BlockPos pos) { - Minecraft mc = Minecraft.getInstance(); - if (mc.level != null) { - waves.add(new WaveEffect(pos, GameClock.millis())); - } - } - - private void spawnHitParticles(LivingEntity target) { - int count = reserveHitParticleBudget(Math.max(0, hitParticleCount.get())); - if (count <= 0) return; - - int baseColor = getColorArgb(target.getId() % 360); - float size = hitParticleSize.get(); - long life = hitParticleLifeMs.get(); - long fadeIn = Math.min(250L, life / 4L); - long fadeOut = Math.min(900L, life / 2L); - - for (int i = 0; i < count; i++) { - if (hitParticles.size() >= HIT_PARTICLE_MAX) { - hitParticles.remove(0); - } - - double px = target.getX() + random.nextDouble() * 0.6 - 0.3; - double py = target.getY() + random.nextDouble() * Math.max(0.2, target.getBbHeight()); - double pz = target.getZ() + random.nextDouble() * 0.6 - 0.3; - - double motion = 0.06; - Vec3 vel = new Vec3( - random.nextDouble() * motion * 2 - motion, - random.nextDouble() * 0.04 - 0.02, - random.nextDouble() * motion * 2 - motion - ); - - int color = hitParticleRandomColor.get() ? randomColor() : baseColor; - ParticleTexture tex = resolveParticleTexture(); - - hitParticles.add(new Particle3D( - new Vec3(px, py, pz), - vel, - life, - fadeIn, - fadeOut, - size, - color, - tex.texture(), - tex.index(), - HIT_PARTICLE_DRAG, - HIT_PARTICLE_GRAVITY, - (random.nextFloat() - 0.5f) * 8f - )); - } - } - - private int reserveHitParticleBudget(int requested) { - Minecraft mc = Minecraft.getInstance(); - if (requested <= 0 || mc.player == null) { - return 0; - } - - int tick = mc.player.tickCount; - if (tick != lastHitParticleTick) { - lastHitParticleTick = tick; - spawnedHitParticlesThisTick = 0; - } - - int remaining = HIT_PARTICLE_SPAWN_CAP_PER_TICK - spawnedHitParticlesThisTick; - if (remaining <= 0) { - return 0; - } - - int granted = Math.min(requested, remaining); - spawnedHitParticlesThisTick += granted; - return granted; - } - - private int randomColor() { - float h = random.nextFloat(); - float s = 0.7f + random.nextFloat() * 0.3f; - float v = 0.8f + random.nextFloat() * 0.2f; - return Color.HSBtoRGB(h, s, v) | 0xFF000000; - } - - private void renderHitParticles(Renderer3D renderer, float tickDelta) { - boolean useDepth = depthTest.get(); - RenderPipeline pipeline = useDepth - ? LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE_LIQUID_IGNORE - : LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE; - Renderer3D.DepthMode depthMode = useDepth ? Renderer3D.DepthMode.PRE_DEPTH : Renderer3D.DepthMode.MAIN; - Quaternionf camRot = RenderState.cameraRotation; - - if (hitParticleTexture.get() == ParticleTextureMode.BLOOM) { - if (FpsOptimizer.bloomDisabled()) return; - MeshBuilder mesh = renderer.batchTextured(pipeline, TextureStorage.FIRE_FLY, depthMode); - if (mesh == null) return; - for (Particle3D p : hitParticles) { - renderParticle(mesh, p, tickDelta, camRot); - } - return; - } - - clearBuckets(hitParticleBuckets); - for (Particle3D p : hitParticles) { - int idx = p.textureIndex(); - if (idx >= 0 && idx < hitParticleBuckets.length) { - hitParticleBuckets[idx].add(p); - } - } - - for (int i = 0; i < hitParticleBuckets.length; i++) { - List bucket = hitParticleBuckets[i]; - if (bucket.isEmpty()) continue; - MeshBuilder mesh = renderer.batchTextured(pipeline, TextureStorage.RANDOM_PARTICLES[i], depthMode); - if (mesh == null) continue; - for (Particle3D p : bucket) { - renderParticle(mesh, p, tickDelta, camRot); - } - } - } - - private void renderParticle(MeshBuilder mesh, Particle3D particle, float tickDelta, Quaternionf camRot) { - Vec3 pos = particle.interpolate(tickDelta); - float alpha = particle.alpha(); - if (alpha <= 0.01f) return; - int argb = applyOpacity(particle.color(), alpha); - addBillboardQuad(mesh, pos.x, pos.y, pos.z, particle.size(), camRot, argb); - } - - private ParticleTexture resolveParticleTexture() { - if (hitParticleTexture.get() == ParticleTextureMode.BLOOM) { - return new ParticleTexture(TextureStorage.FIRE_FLY, -1); - } - int idx = random.nextInt(TextureStorage.RANDOM_PARTICLES.length); - return new ParticleTexture(TextureStorage.RANDOM_PARTICLES[idx], idx); - } - - private boolean isBubblesMode() { - return effectMode.get() == EffectMode.BUBBLES; - } - - private boolean isWaveMode() { - return effectMode.get() == EffectMode.WAVE; - } - - private boolean isImpactMode() { - return effectMode.get() == EffectMode.IMPACT; - } - - private boolean usesSecondaryColor() { - return AnimatedRenderColors.usesSecondary(animatedColorMode()); - } - - private int getColorArgb(int count) { - return AnimatedRenderColors.resolve( - animatedColorMode(), - colorSpeed.get(), - count, - colorValue.getArgb(), - colorValue2.getArgb() - ); - } - - private AnimatedRenderColors.Mode animatedColorMode() { - return AnimatedColorModeOptions.parse(colorMode.get()); - } - - private boolean isCritical(Player attacker, float cooldown) { - if (cooldown < CHARGED_THRESHOLD) return false; - if (attacker.onGround()) return false; - if (attacker.onClimbable()) return false; - if (attacker.isInWater()) return false; - if (attacker.isPassenger()) return false; - return attacker.fallDistance > 0.0f; - } - - private void spawnStars(Vec3 pos, boolean crit) { - int count = crit ? CRIT_STARS : NORMAL_STARS; - int color = crit ? critStarsColor.getArgb() : starsColor.getArgb(); - - for (int i = 0; i < count; i++) { - double angle = random.nextDouble() * Math.PI * 2.0; - double speed = 0.14 + random.nextDouble() * 0.14; - double vx = Math.cos(angle) * speed; - double vz = Math.sin(angle) * speed; - double vy = 0.12 + random.nextDouble() * 0.12; - - long life = 500 + random.nextInt(250); - float size = 0.18f + random.nextFloat() * 0.12f; - - stars.add(new Star(pos, new Vec3(vx, vy, vz), life, size, color)); - } - } - - public enum EffectMode { - BUBBLES, - WAVE, - IMPACT - } - - private record ParticleTexture(Identifier texture, int index) { - } - - private record HitBubble(Vec3 pos, float yaw, float pitch, Timer life) { - } - - // textured rendering handled by Renderer3D batching - - private static class Star { - private final Timer timer; - private final long lifeMs; - private final float size; - private final int color; - private Vec3 pos; - private Vec3 prevPos; - private Vec3 velocity; - - private Star(Vec3 pos, Vec3 velocity, long lifeMs, float size, int color) { - this.pos = pos; - this.prevPos = pos; - this.velocity = velocity; - this.timer = new Timer(); - this.lifeMs = lifeMs; - this.size = size; - this.color = color; - } - - private boolean update() { - prevPos = pos; - pos = pos.add(velocity); - velocity = velocity.multiply(0.94, 0.94, 0.94).add(0.0, -0.03, 0.0); - return timer.passedMs(lifeMs); - } - - private Vec3 interpolate(float tickDelta) { - return prevPos.lerp(pos, tickDelta); - } - - private float getLifeProgress() { - return Math.min(1f, timer.getPassedTimeMs() / (float) lifeMs); - } - } - - private class WaveEffect { - private final BlockPos centerPos; - private final long startTime; - - private WaveEffect(BlockPos centerPos, long startTime) { - this.centerPos = centerPos; - this.startTime = startTime; - } - - private boolean isExpired() { - return GameClock.millis() - startTime > Math.max(1L, waveDurationMs.get()); - } - - private void render(Renderer3D renderer) { - Minecraft mc = Minecraft.getInstance(); - if (mc.level == null) return; - if (renderer == null) return; - - long durationMs = Math.max(1L, waveDurationMs.get()); - int maxRadius = Math.max(1, waveRadius.get()); - long elapsed = GameClock.millis() - startTime; - float progress = Mth.clamp((float) elapsed / durationMs, 0f, 1f); - - float currentRadius = progress * maxRadius; - float globalAlpha = (float) Math.pow(1.0f - progress, 0.6); - - float minRadSq = (currentRadius - WAVE_WIDTH) * (currentRadius - WAVE_WIDTH); - float maxRadSq = (currentRadius + 0.5f) * (currentRadius + 0.5f); - - float prevWidth = RenderState.lineWidth; - RenderState.lineWidth = Math.max(0.5f, WAVE_LINE_WIDTH_BASE + globalAlpha * WAVE_LINE_WIDTH_BOOST); - try { - boolean useDepth = depthTest.get(); - RenderPipeline pipeline = useDepth - ? LoVisualRenderPipelines.WORLD_COLORED_LINES_LIQUID_IGNORE - : LoVisualRenderPipelines.WORLD_COLORED_LINES; - Renderer3D.DepthMode depthMode = useDepth ? Renderer3D.DepthMode.PRE_DEPTH : Renderer3D.DepthMode.MAIN; - MeshBuilder mesh = renderer.batch(pipeline, depthMode); - if (mesh == null) return; - - int rendered = 0; - - for (int x = -maxRadius; x <= maxRadius; x++) { - for (int z = -maxRadius; z <= maxRadius; z++) { - if (rendered >= WAVE_MAX_PER_FRAME) return; - - float distSq = x * x + z * z; - if (distSq < minRadSq || distSq > maxRadSq) continue; - - BlockPos checkPos = centerPos.offset(x, 0, z); - BlockPos renderPos = findSurface(checkPos); - if (renderPos == null) continue; - - BlockState state = mc.level.getBlockState(renderPos); - VoxelShape shape = state.getShape(mc.level, renderPos); - if (shape.isEmpty()) continue; - - rendered++; - - float distance = (float) Math.sqrt(distSq); - float localAlpha = 1.0f - Math.abs(distance - currentRadius) / WAVE_WIDTH; - localAlpha = Math.max(0f, Math.min(1f, localAlpha)) * globalAlpha; - if (localAlpha <= WAVE_MIN_ALPHA) continue; - - int colorIndex = (int) (Math.toDegrees(Math.atan2(z, x)) + 180.0); - int color = applyOpacity(getColorArgb(colorIndex), localAlpha); - - addShapeOutline(mesh, renderPos, shape, color, WAVE_OUTLINE_EPS); - } - } - } finally { - RenderState.lineWidth = prevWidth; - } - } - - private BlockPos findSurface(BlockPos pos) { - Minecraft mc = Minecraft.getInstance(); - for (int y = 2; y >= -4; y--) { - BlockPos p = pos.above(y); - if (!mc.level.getBlockState(p).isAir() && mc.level.getBlockState(p.above()).isAir()) { - return p; - } - } - return null; - } - } - - /** - * Кольцевая волна + вертикальная "стена" свечения в точке удара — реализация - * идеи ImpactRing (JumpWaveRenderer/JumpGlowRenderer, экранный SSR-варп в - * референсе): у нас проще, обычными аддитивными квадами через MeshBuilder, - * без захвата сцены — консистентно с остальными эффектами файла. - */ - private class ImpactRing { - private final Vec3 center; - private final long startTime; - - private ImpactRing(Vec3 center, long startTime) { - this.center = center; - this.startTime = startTime; - } - - private boolean isExpired() { - return GameClock.millis() - startTime > Math.max(1L, impactDurationMs.get()); - } - - private void render(Renderer3D renderer) { - long durationMs = Math.max(1L, impactDurationMs.get()); - long elapsed = GameClock.millis() - startTime; - float progress = Mth.clamp((float) elapsed / durationMs, 0f, 1f); - float fade = (float) Math.pow(1.0 - progress, 1.4); - if (fade <= 0.01f) return; - - float maxRadius = Math.max(0.1f, impactRadius.get()); - float radius = maxRadius * (0.15f + 0.85f * progress); - int color = getColorArgb((int) (progress * 360)); - - MeshBuilder ring = renderer.batch(LoVisualRenderPipelines.WORLD_COLORED_ADDITIVE, Renderer3D.DepthMode.MAIN); - if (ring != null) { - renderRing(ring, radius, fade, color); - } - - MeshBuilder glow = renderer.batch(LoVisualRenderPipelines.WORLD_COLORED_ADDITIVE_DEPTH, Renderer3D.DepthMode.PRE_DEPTH); - if (glow != null) { - renderGlowWall(glow, radius, fade * 0.6f, color); - } - } - - private void renderRing(MeshBuilder mesh, float radius, float fade, int color) { - float inner = radius * 0.72f; - for (int i = 0; i < IMPACT_RING_SEGMENTS; i++) { - double a0 = (Math.PI * 2.0 / IMPACT_RING_SEGMENTS) * i; - double a1 = (Math.PI * 2.0 / IMPACT_RING_SEGMENTS) * (i + 1); - - float ix0 = (float) (Math.cos(a0) * inner); - float iz0 = (float) (Math.sin(a0) * inner); - float ix1 = (float) (Math.cos(a1) * inner); - float iz1 = (float) (Math.sin(a1) * inner); - float ox0 = (float) (Math.cos(a0) * radius); - float oz0 = (float) (Math.sin(a0) * radius); - float ox1 = (float) (Math.cos(a1) * radius); - float oz1 = (float) (Math.sin(a1) * radius); - - int edgeColor = applyOpacity(color, fade); - int fadeColor = applyOpacity(color, 0f); - - mesh.ensureQuadCapacity(); - int i1 = mesh.vec3(center.x + ix0, center.y + 0.02, center.z + iz0).color(new RenderColor(fadeColor)).next(); - int i2 = mesh.vec3(center.x + ix1, center.y + 0.02, center.z + iz1).color(new RenderColor(fadeColor)).next(); - int i3 = mesh.vec3(center.x + ox1, center.y + 0.02, center.z + oz1).color(new RenderColor(edgeColor)).next(); - int i4 = mesh.vec3(center.x + ox0, center.y + 0.02, center.z + oz0).color(new RenderColor(edgeColor)).next(); - mesh.quad(i1, i2, i3, i4); - } - } - - private void renderGlowWall(MeshBuilder mesh, float radius, float fade, int color) { - float height = impactGlowHeight.get() * (1.0f - fade * 0.3f); - for (int i = 0; i < IMPACT_RING_SEGMENTS; i++) { - double a0 = (Math.PI * 2.0 / IMPACT_RING_SEGMENTS) * i; - double a1 = (Math.PI * 2.0 / IMPACT_RING_SEGMENTS) * (i + 1); - - float x0 = (float) (Math.cos(a0) * radius); - float z0 = (float) (Math.sin(a0) * radius); - float x1 = (float) (Math.cos(a1) * radius); - float z1 = (float) (Math.sin(a1) * radius); - - int baseColor = applyOpacity(color, fade); - int topColor = applyOpacity(color, 0f); - - mesh.ensureQuadCapacity(); - int i1 = mesh.vec3(center.x + x0, center.y, center.z + z0).color(new RenderColor(baseColor)).next(); - int i2 = mesh.vec3(center.x + x1, center.y, center.z + z1).color(new RenderColor(baseColor)).next(); - int i3 = mesh.vec3(center.x + x1, center.y + height, center.z + z1).color(new RenderColor(topColor)).next(); - int i4 = mesh.vec3(center.x + x0, center.y + height, center.z + z0).color(new RenderColor(topColor)).next(); - mesh.quad(i1, i2, i3, i4); - } - } - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/Crosshair.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/Crosshair.java deleted file mode 100644 index bd1b2a7..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/Crosshair.java +++ /dev/null @@ -1,743 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.features.module.modules.visuals.player; - -import dev.loki.lovisual.features.module.core.Module; -import dev.loki.lovisual.features.module.core.ModuleCategory; -import dev.loki.lovisual.features.module.core.ModuleInfo; -import dev.loki.lovisual.features.module.modules.visuals.player.Crosshair; -import dev.loki.lovisual.features.module.modules.visuals.weather.WorldTweaks; -import dev.loki.lovisual.features.module.phase.HudPhase; -import dev.loki.lovisual.features.module.core.Module; -import dev.loki.lovisual.render.engine.core.context.ViewportContext; -import dev.loki.lovisual.util.item.food.FoodUtil; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.core.BlockPos; -import net.minecraft.core.component.DataComponents; -import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.util.Mth; -import net.minecraft.world.entity.Entity; -import net.minecraft.world.entity.EquipmentSlot; -import net.minecraft.world.entity.MobCategory; -import net.minecraft.world.entity.ai.attributes.Attributes; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.item.*; -import net.minecraft.world.item.enchantment.Enchantments; -import net.minecraft.world.item.enchantment.ItemEnchantments; -import net.minecraft.world.item.equipment.Equippable; -import net.minecraft.world.level.Level; -import net.minecraft.world.phys.EntityHitResult; -import net.minecraft.world.phys.HitResult; -import dev.loki.lovisual.config.values.primitive.BooleanValue; -import dev.loki.lovisual.config.values.mode.EnumValue; -import dev.loki.lovisual.config.values.primitive.NumberValue; -import dev.loki.lovisual.config.values.color.RGBAColorValue; -import dev.loki.lovisual.config.common.CommonSettingSchemas; -import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; -import dev.loki.lovisual.features.gui.hud.draggable.impl.combat.Itemizer; -import dev.loki.lovisual.features.relations.category.CategoryService; -import dev.loki.lovisual.features.relations.EntityFilters; -import dev.loki.lovisual.features.relations.PlayerRelations; -import dev.loki.lovisual.mixininterface.entity.IPlayerAttackCooldown; -import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors; -import dev.loki.lovisual.render.engine.animation.AnimationUtility; -import dev.loki.lovisual.render.engine.color.RenderColor; -import dev.loki.lovisual.render.engine.math.ColorMath; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.text.font.info.FontInfo; -import dev.loki.lovisual.render.engine.text.font.Fonts; -import dev.loki.lovisual.render.engine.text.render.TextRenderer; -import dev.loki.lovisual.features.module.modules.visuals.weather.WorldTweaks; - -import java.util.List; - -//todo Description -@ModuleInfo( - id = "crosshair", - displayName = "Crosshair", - category = ModuleCategory.VISUALS -) -public class Crosshair extends Module { - - private static final int DEFAULT_COLOR = 0xFFFFFFFF; - private static final int OUTLINE_COLOR = 0xFF000000; - private static final int RIPTIDE_BLOCK_COLOR = 0xFFFF3B3B; - private static final float OUTLINE_EXPAND = 0.5f; - private static final float GAP_SMOOTHING = 0.15f; - private static final float COLOR_SMOOTH_SPEED = 14.0f; - private static final float ORBIZ_PROGRESS_SMOOTH_SPEED = 22.0f; - private static final float COOLDOWN_LEAD = 0.06f; - private static final float RIPTIDE_CROSS_LENGTH_FACTOR = 0.65f; - private final EnumValue crosshairType = - enumSetting("crosshairType", "type", CrosshairType.CLASSIC); - private final EnumValue orbizMotion = - visibleWhen(enumSetting("crosshairOrbizMotion", "orbiz_motion", OrbizMotionMode.DYNAMIC), this::isOrbiz); - private final BooleanValue showOutline = visibleWhen( - bool("crosshairOutline", "outline", true), - this::isClassicCrosshair - ); - private final BooleanValue dynamicGap = visibleWhen( - bool("crosshairDynamicGap", "dynamic_gap", true), - this::isClassicCrosshair - ); - private final BooleanValue highlightTarget = bool("crosshairHighlightTarget", "highlight_target", true); - private final NumberValue thickness = visibleWhen( - num("crosshairThickness", "thickness", 0.5, 0.5, 1.0), - this::isClassicCrosshair - ); - private final NumberValue length = visibleWhen( - num("crosshairLength", "length", 3.0, 1.0, 10.0), - this::isClassicCrosshair - ); - private final NumberValue baseGap = visibleWhen( - num("crosshairBaseGap", "base_gap", 4.0, 0.0, 10.0), - this::isClassicCrosshair - ); - private final NumberValue maxGapIncrease = visibleWhen( - num("crosshairMaxGapIncrease", "max_gap_increase", 7.0, 0.0, 15.0), - () -> isClassicCrosshair() && dynamicGap.get() - ); - private final NumberValue orbizRadius = visibleWhen( - num("crosshairOrbizRadius", "orbiz_radius", 5.0, 2.0, 5.0), - this::isOrbiz - ); - private final NumberValue orbizThickness = visibleWhen( - num("crosshairOrbizThickness", "orbiz_thickness", 1.35, 0.5, 5.0), - this::isOrbiz - ); - private final NumberValue orbizSoftness = visibleWhen( - num("crosshairOrbizSoftness", "orbiz_softness", 0.35, 0.0, 2.0), - this::isOrbiz - ); - private final NumberValue orbizGlow = visibleWhen( - num("crosshairOrbizGlow", "orbiz_glow", 5.5, 0.0, 20.0), - this::isOrbiz - ); - private final NumberValue orbizGlowStrength = visibleWhen( - num("crosshairOrbizGlowStrength", "orbiz_glow_strength", 0.72, 0.0, 2.0), - this::isOrbiz - ); - private final NumberValue orbizCooldownMultiplier = visibleWhen( - num("crosshairOrbizCooldownMultiplier", "orbiz_cooldown_multiplier", 1.0, 0.1, 1.5), - this::isOrbiz - ); - private final NumberValue orbizTrackAlpha = visibleWhen( - num("crosshairOrbizTrackAlpha", "orbiz_track_alpha", 0.24, 0.0, 1.0), - this::isOrbiz - ); - private final NumberValue orbizBodyAlpha = visibleWhen( - num("crosshairOrbizBodyAlpha", "orbiz_body_alpha", 0.92, 0.0, 1.0), - this::isOrbiz - ); - private final NumberValue orbizHeadBoost = visibleWhen( - num("crosshairOrbizHeadBoost", "orbiz_head_boost", 0.30, 0.0, 1.0), - this::isOrbiz - ); - private final NumberValue orbizAngularCycles = visibleWhen( - num("crosshairOrbizAngularCycles", "orbiz_angular_cycles", 1.0, 0.25, 4.0), - this::isOrbiz - ); - private final EnumValue orbizColorMode = visibleWhen( - common(enumSetting( - "crosshairOrbizColorMode", - "orbiz_color_mode", - AnimatedRenderColors.Mode.RAINBOW, - AnimatedRenderColors.Mode.STATIC, - AnimatedRenderColors.Mode.RAINBOW, - AnimatedRenderColors.Mode.LIGHT_RAINBOW, - AnimatedRenderColors.Mode.SKY, - AnimatedRenderColors.Mode.FADE, - AnimatedRenderColors.Mode.DOUBLE_COLOR, - AnimatedRenderColors.Mode.ANALOGOUS, - AnimatedRenderColors.Mode.THEME - ), CommonSettingSchemas.RENDER_COLOR_MODE.commonI18nKey()), - this::isOrbiz - ); - private final NumberValue orbizColorSpeed = visibleWhen( - common(num("crosshairOrbizColorSpeed", "orbiz_color_speed", 18, 2, 54), - CommonSettingSchemas.RENDER_COLOR_SPEED.commonI18nKey()), - this::isOrbiz - ); - private final RGBAColorValue orbizColor = visibleWhen( - common(color("crosshairOrbizColor", "orbiz_color", "#EEFFFFFF"), - CommonSettingSchemas.RENDER_PRIMARY_COLOR.commonI18nKey()), - this::isOrbiz - ); - private final RGBAColorValue orbizColor2 = visibleWhen( - common(color("crosshairOrbizColor2", "orbiz_color2", "#EE55FFFF"), - CommonSettingSchemas.RENDER_SECONDARY_COLOR.commonI18nKey()), - () -> isOrbiz() && AnimatedRenderColors.usesSecondary(orbizColorMode.get()) - ); - private final NumberValue orbizLookMultiplier = visibleWhen( - num("crosshairOrbizLookMultiplier", "orbiz_look_multiplier", 0.45, 0.0, 2.5), - () -> isOrbiz() && isOrbizMotionDynamic() - ); - private final NumberValue orbizMoveMultiplier = visibleWhen( - num("crosshairOrbizMoveMultiplier", "orbiz_move_multiplier", 2.0, 0.0, 10.0), - () -> isOrbiz() && isOrbizMotionDynamic() - ); - private final NumberValue orbizMotionLimit = visibleWhen( - num("crosshairOrbizMotionLimit", "orbiz_motion_limit", 8.0, 0.0, 24.0), - () -> isOrbiz() && isOrbizMotionDynamic() - ); - private final NumberValue orbizMotionSmoothing = visibleWhen( - num("crosshairOrbizMotionSmoothing", "orbiz_motion_smoothing", 12.0, 1.0, 30.0), - () -> isOrbiz() && isOrbizMotionDynamic() - ); - private float animatedGap; - private int animatedColor = DEFAULT_COLOR; - private float animatedOrbizProgress = 1.0f; - private float orbizOffsetX; - private float orbizOffsetY; - private float lastOrbizYaw; - private float lastOrbizPitch; - private boolean orbizMotionInitialized; - private boolean colorInitialized; - private boolean chestInit; - private boolean lastSwapCandidate; - private boolean lastChestWasElytra; - private ItemStack lastChestStack = ItemStack.EMPTY; - - private static int colorDistance(int a, int b) { - int dr = Math.abs(((a >>> 16) & 0xFF) - ((b >>> 16) & 0xFF)); - int dg = Math.abs(((a >>> 8) & 0xFF) - ((b >>> 8) & 0xFF)); - int db = Math.abs((a & 0xFF) - (b & 0xFF)); - int da = Math.abs(((a >>> 24) & 0xFF) - ((b >>> 24) & 0xFF)); - return dr + dg + db + da; - } - - public static boolean isRiptideBlocked(Player player, ItemStack stack) { - if (player == null || stack == null || stack.isEmpty()) return false; - if (!stack.is(Items.TRIDENT)) return false; - if (!hasRiptide(stack)) return false; - return !isTouchingWaterOrServerRain(player); - } - - private static boolean hasRiptide(ItemStack stack) { - ItemEnchantments ench = stack.getOrDefault( - DataComponents.ENCHANTMENTS, - ItemEnchantments.EMPTY - ); - return ench.keySet().stream().anyMatch(entry -> { - var keyOpt = entry.unwrapKey(); - return keyOpt.isPresent() && keyOpt.get().identifier().equals(Enchantments.RIPTIDE.identifier()); - }); - } - - private static boolean isTouchingWaterOrServerRain(Player player) { - if (player == null) return false; - if (player.isInWater()) return true; - Level world = player.level(); - BlockPos pos = player.blockPosition(); - if (WorldTweaks.hasServerRainAt(world, pos)) return true; - BlockPos top = BlockPos.containing(pos.getX(), player.getBoundingBox().maxY, pos.getZ()); - return WorldTweaks.hasServerRainAt(world, top); - } - - @Override - public HudPhase getHudPhase() { - return HudPhase.LAST; - } - - public boolean shouldHideVanilla() { - Minecraft mc = Minecraft.getInstance(); - return isEnabled() - && mc.player != null - && mc.level != null - && mc.options.getCameraType().isFirstPerson(); - } - - @Override - public void onEnable() { - resetSwapDetector(); - resetOrbizState(); - colorInitialized = false; - } - - @Override - public void onDisable() { - resetSwapDetector(); - resetOrbizState(); - Itemizer.hideElytraSwap(); - } - - @Override - public void onRenderHudEngine(Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta) { - Minecraft mc = Minecraft.getInstance(); - if (!isEnabled() || mc.player == null || mc.level == null) return; - if (!mc.options.getCameraType().isFirstPerson()) return; - - float scale = ViewportContext.getScaleFactor(); - float centerX = mc.getWindow().getWidth() * 0.5f; - float centerY = mc.getWindow().getHeight() * 0.5f; - - float thicknessPx = (float) (thickness.get() * scale); - float lengthPx = (float) (length.get() * scale); - float outlinePx = OUTLINE_EXPAND * scale; - - float cooldown = 0.0f; - if (dynamicGap.get()) { - float charge = getChargeProgress(mc.player); - if (charge >= 0.0f) { - cooldown = 1.0f - charge; - } else { - float progress = getAttackCooldownProgress(); - progress = Mth.clamp(progress + COOLDOWN_LEAD, 0.0f, 1.0f); - cooldown = 1.0f - progress; - } - } - - float targetGap = (float) (baseGap.get() + maxGapIncrease.get() * cooldown); - animatedGap += (targetGap - animatedGap) * GAP_SMOOTHING; - float gapPx = animatedGap * scale; - - updateSwapDetector(mc.player); - - if (crosshairType.get() == CrosshairType.ORBIZ) { - renderOrbiz(renderer, centerX, centerY, scale); - float infoGapPx = (float) (orbizRadius.get() * scale); - float infoLengthPx = (float) ((orbizGlow.get() + orbizThickness.get()) * scale); - return; - } - - boolean outline = showOutline.get(); - - if (crosshairType.get() == CrosshairType.DOT) { - int dotColor = smoothCrosshairColor(resolveCrosshairTargetColor()); - float dotSize = thicknessPx * 2.0f; - drawSegment(renderer, centerX - dotSize * 0.5, centerY - dotSize * 0.5, dotSize, dotSize, dotColor, outline, outlinePx); - return; - } - - if (crosshairType.get() == CrosshairType.PINWHEEL) { - int pinwheelColor = smoothCrosshairColor(resolveCrosshairTargetColor()); - renderPinwheel(renderer, centerX, centerY, thicknessPx, lengthPx, pinwheelColor); - return; - } - if (shouldShowRiptideBlocked(mc.player) || shouldShowCooldownCross(mc.player)) { - int dangerColor = smoothCrosshairColor(RIPTIDE_BLOCK_COLOR); - renderRiptideBlockedCross(renderer, centerX, centerY, thicknessPx, lengthPx, outline, outlinePx, dangerColor); - return; - } - - int color = smoothCrosshairColor(resolveCrosshairTargetColor()); - boolean tClassic = crosshairType.get() == CrosshairType.T_CLASSIC; - - if (tClassic) { - double barX = centerX - lengthPx; - double barY = centerY - thicknessPx * 0.5f; - double barW = lengthPx * 2.0f; - double stemX = centerX - thicknessPx * 0.5f; - double stemH = lengthPx + gapPx; - drawSegment(renderer, barX, barY, barW, thicknessPx, color, outline, outlinePx); - drawSegment(renderer, stemX, centerY, thicknessPx, stemH, color, outline, outlinePx); - return; - } - - double topX = centerX - thicknessPx * 0.5f; - double topY = centerY - gapPx - lengthPx; - double bottomX = centerX - thicknessPx * 0.5f; - double bottomY = centerY + gapPx; - double leftX = centerX - gapPx - lengthPx; - double leftY = centerY - thicknessPx * 0.5f; - double rightX = centerX + gapPx; - double rightY = centerY - thicknessPx * 0.5f; - - drawSegment(renderer, topX, topY, thicknessPx, lengthPx, color, outline, outlinePx); - drawSegment(renderer, bottomX, bottomY, thicknessPx, lengthPx, color, outline, outlinePx); - drawSegment(renderer, leftX, leftY, lengthPx, thicknessPx, color, outline, outlinePx); - drawSegment(renderer, rightX, rightY, lengthPx, thicknessPx, color, outline, outlinePx); - } - - private float getAttackCooldownProgress() { - Minecraft mc = Minecraft.getInstance(); - if (mc.player instanceof IPlayerAttackCooldown cooldown) { - return cooldown.lovisual$getAttackCooldownProgress(0.0f); - } - return mc.player.getAttackStrengthScale(0.0f); - } - - private float getChargeProgress(Player player) { - if (player == null || !player.isUsingItem()) return -1.0f; - ItemStack stack = player.getUseItem(); - if (stack == null || stack.isEmpty()) return -1.0f; - - int useTicks = player.getTicksUsingItem(); - Item item = stack.getItem(); - - if (item instanceof BowItem) { - return Mth.clamp(BowItem.getPowerForTime(useTicks), 0.0f, 1.0f); - } - if (item instanceof CrossbowItem) { - int pullTime = CrossbowItem.getChargeDuration(stack, player); - if (pullTime <= 0) return 1.0f; - return Mth.clamp(useTicks / (float) pullTime, 0.0f, 1.0f); - } - if (stack.is(Items.TRIDENT)) { - return Mth.clamp(useTicks / 10.0f, 0.0f, 1.0f); - } - - return -1.0f; - } - - /** - * Four-blade pinwheel crosshair. Unlike Classic/T-Classic (which open a gap on attack), - * this one spins: fully wound up (0°) at full charge, unwinding toward - * {@link #PINWHEEL_SPIN_DEGREES} as the attack cooldown resets. - */ - private static final float PINWHEEL_SPIN_DEGREES = 130f; - - private void renderPinwheel(Renderer2D renderer, float centerX, float centerY, float thicknessPx, float lengthPx, int color) { - float chargeProgress = Mth.clamp(getAttackCooldownProgress(), 0.0f, 1.0f); - float spinDeg = (1.0f - chargeProgress) * PINWHEEL_SPIN_DEGREES; - - for (int i = 0; i < 4; i++) { - float bladeAngleDeg = 45f + i * 90f + spinDeg; - drawRotatedBlade(renderer, centerX, centerY, thicknessPx, lengthPx, bladeAngleDeg, color); - } - } - - private void drawRotatedBlade(Renderer2D renderer, float centerX, float centerY, - float thicknessPx, float lengthPx, float angleDeg, int color) { - double rad = Math.toRadians(angleDeg); - double dirX = Math.cos(rad); - double dirY = Math.sin(rad); - double perpX = -dirY; - double perpY = dirX; - - double innerX = centerX; - double innerY = centerY; - double outerX = centerX + dirX * lengthPx; - double outerY = centerY + dirY * lengthPx; - double halfThickness = thicknessPx * 0.5; - - double[] points = new double[]{ - innerX - perpX * halfThickness, innerY - perpY * halfThickness, - outerX - perpX * halfThickness, outerY - perpY * halfThickness, - outerX + perpX * halfThickness, outerY + perpY * halfThickness, - innerX + perpX * halfThickness, innerY + perpY * halfThickness - }; - renderer.polygon(points, 4, color); - } - - private void renderOrbiz(Renderer2D renderer, float centerX, float centerY, float scale) { - Minecraft mc = Minecraft.getInstance(); - boolean danger = shouldShowRiptideBlocked(mc.player) || shouldShowCooldownCross(mc.player); - float progress = danger ? 1.0f : resolveOrbizProgress(); - float targetProgress = Mth.clamp(progress * orbizCooldownMultiplier.get().floatValue(), 0.0f, 1.0f); - if (targetProgress < animatedOrbizProgress) { - animatedOrbizProgress = targetProgress; - } else { - float t = AnimationUtility.clamp01(AnimationUtility.deltaTime() * ORBIZ_PROGRESS_SMOOTH_SPEED); - animatedOrbizProgress = AnimationUtility.lerp(animatedOrbizProgress, targetProgress, t); - } - - updateOrbizMotion(); - - AnimatedRenderColors.Mode mode = danger ? AnimatedRenderColors.Mode.STATIC : orbizColorMode.get(); - int primary = danger ? colorWithConfigAlpha(RIPTIDE_BLOCK_COLOR, orbizColor.getArgb()) : resolveOrbizPrimary(mode); - int secondary = danger ? primary : AnimatedRenderColors.angularSecondaryColor(mode, primary, orbizColor2.getArgb()); - primary = AnimatedRenderColors.angularPrimaryColor(mode, primary); - - float radiusPx = (float) (orbizRadius.get() * scale); - float thicknessPx = (float) (orbizThickness.get() * scale); - float softnessPx = (float) (orbizSoftness.get() * scale); - float glowPx = (float) (orbizGlow.get() * scale); - float sweep = animatedOrbizProgress * 360.0f; - float trackGlow = glowPx > 0.0f ? 1.0f : 0.0f; - - renderer.orbizRing( - centerX + orbizOffsetX * scale, - centerY + orbizOffsetY * scale, - radiusPx, - thicknessPx, - 0.0f, - sweep, - softnessPx, - glowPx, - orbizGlowStrength.get().floatValue(), - primary, - secondary, - AnimatedRenderColors.shaderMode(mode), - AnimatedRenderColors.angularOffset01(mode, orbizColorSpeed.get()), - orbizAngularCycles.get().floatValue(), - orbizTrackAlpha.get().floatValue(), - orbizBodyAlpha.get().floatValue(), - orbizHeadBoost.get().floatValue(), - trackGlow, - true - ); - } - - private float resolveOrbizProgress() { - Minecraft mc = Minecraft.getInstance(); - float charge = getChargeProgress(mc.player); - if (charge >= 0.0f) { - return Mth.clamp(charge, 0.0f, 1.0f); - } - return Mth.clamp(getAttackCooldownProgress() + COOLDOWN_LEAD, 0.0f, 1.0f); - } - - private void updateOrbizMotion() { - Minecraft mc = Minecraft.getInstance(); - if (mc.player == null) { - orbizOffsetX = AnimationUtility.fast(orbizOffsetX, 0.0f, 10.0f); - orbizOffsetY = AnimationUtility.fast(orbizOffsetY, 0.0f, 10.0f); - orbizMotionInitialized = false; - return; - } - - float yaw = mc.player.getYRot(); - float pitch = mc.player.getXRot(); - if (!orbizMotionInitialized) { - lastOrbizYaw = yaw; - lastOrbizPitch = pitch; - orbizMotionInitialized = true; - } - - float targetX = 0.0f; - float targetY = 0.0f; - if (isOrbizMotionDynamic()) { - float look = orbizLookMultiplier.get().floatValue(); - float move = orbizMoveMultiplier.get().floatValue(); - targetX = Mth.wrapDegrees(lastOrbizYaw - yaw) * look + mc.player.xxa * move; - targetY = (lastOrbizPitch - pitch) * look + mc.player.zza * move; - float limit = orbizMotionLimit.get().floatValue(); - targetX = Mth.clamp(targetX, -limit, limit); - targetY = Mth.clamp(targetY, -limit, limit); - } - - float t = AnimationUtility.clamp01(AnimationUtility.deltaTime() * orbizMotionSmoothing.get().floatValue()); - orbizOffsetX = AnimationUtility.lerp(orbizOffsetX, targetX, t); - orbizOffsetY = AnimationUtility.lerp(orbizOffsetY, targetY, t); - lastOrbizYaw = yaw; - lastOrbizPitch = pitch; - } - - private int resolveOrbizPrimary(AnimatedRenderColors.Mode mode) { - int configured = orbizColor.getArgb(); - if (mode == AnimatedRenderColors.Mode.STATIC && highlightTarget.get()) { - return colorWithConfigAlpha(smoothCrosshairColor(resolveCrosshairTargetColor()), configured); - } - return configured; - } - - private int colorWithConfigAlpha(int rgbSource, int alphaSource) { - return ColorMath.colorWithAlpha(rgbSource, (alphaSource >>> 24) & 0xFF); - } - - private boolean isOrbiz() { - return crosshairType.get() == CrosshairType.ORBIZ; - } - - private boolean isClassicCrosshair() { - return crosshairType.get() != CrosshairType.ORBIZ; - } - - private boolean isOrbizMotionDynamic() { - return orbizMotion.get() == OrbizMotionMode.DYNAMIC; - } - - private void resetOrbizState() { - animatedOrbizProgress = 1.0f; - orbizOffsetX = 0.0f; - orbizOffsetY = 0.0f; - lastOrbizYaw = 0.0f; - lastOrbizPitch = 0.0f; - orbizMotionInitialized = false; - } - - private int resolveCrosshairTargetColor() { - Minecraft mc = Minecraft.getInstance(); - if (!highlightTarget.get()) return DEFAULT_COLOR; - HitResult hit = mc.hitResult; - if (!(hit instanceof EntityHitResult ehr)) return DEFAULT_COLOR; - Entity entity = ehr.getEntity(); - if (entity == null) return DEFAULT_COLOR; - return resolveEntityColor(entity); - } - - private int smoothCrosshairColor(int targetColor) { - if (!colorInitialized) { - animatedColor = targetColor; - colorInitialized = true; - return animatedColor; - } - float t = AnimationUtility.clamp01(AnimationUtility.deltaTime() * COLOR_SMOOTH_SPEED); - animatedColor = HudRenderUtil.mixColor(animatedColor, targetColor, t); - if (colorDistance(animatedColor, targetColor) <= 2) { - animatedColor = targetColor; - } - return animatedColor; - } - - private int resolveEntityColor(Entity entity) { - if (entity instanceof Player player) { - return CategoryService.getColor(player); - } - - PlayerRelations rel = PlayerRelations.get(); - boolean friendly = isFriendlyEntity(entity); - return friendly ? rel.colorFriend() : rel.colorEnemy(); - } - - private boolean isFriendlyEntity(Entity entity) { - var id = BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()); - if (id != null && EntityFilters.get().isIgnoredEntity(id.toString())) { - return true; - } - return entity.getType().getCategory() != MobCategory.MONSTER; - } - - private boolean shouldShowRiptideBlocked(Player player) { - if (player == null) return false; - ItemStack main = player.getMainHandItem(); - if (isRiptideBlocked(player, main)) return true; - if (!isHandLikeItem(main)) return false; - return isRiptideBlocked(player, player.getOffhandItem()); - } - - private boolean shouldShowCooldownCross(Player player) { - if (player == null) return false; - ItemStack main = player.getMainHandItem(); - if (isCooldownCrossItem(main) && player.getCooldowns().isOnCooldown(main)) return true; - if (shouldSuppressOffhandCooldown(main)) return false; - ItemStack off = player.getOffhandItem(); - return isCooldownCrossItem(off) && player.getCooldowns().isOnCooldown(off); - } - - private boolean isCooldownCrossItem(ItemStack stack) { - return stack != null && !stack.isEmpty(); - } - - private boolean shouldSuppressOffhandCooldown(ItemStack main) { - return !isHandLikeItem(main); - } - - private boolean isHandLikeItem(ItemStack stack) { - if (stack == null || stack.isEmpty()) return true; - Item item = stack.getItem(); - if (item instanceof BowItem || item instanceof CrossbowItem) return false; - if (stack.is(Items.TRIDENT)) return false; - if (FoodUtil.isFood(stack)) return false; - - final boolean[] hasAttackMod = {false}; - stack.forEachModifier(net.minecraft.world.entity.EquipmentSlotGroup.MAINHAND, (attribute, modifier, display) -> { - if (attribute.equals(Attributes.ATTACK_DAMAGE) || attribute.equals(Attributes.ATTACK_SPEED)) { - hasAttackMod[0] = true; - } - }); - return !hasAttackMod[0]; - } - - private void renderRiptideBlockedCross(Renderer2D renderer, - float centerX, - float centerY, - float thicknessPx, - float lengthPx, - boolean outline, - float outlinePx, - int color) { - float crossLength = lengthPx * RIPTIDE_CROSS_LENGTH_FACTOR; - double hX = centerX - crossLength; - double hY = centerY - thicknessPx * 0.5f; - double vX = centerX - thicknessPx * 0.5f; - double vY = centerY - crossLength; - - drawSegment(renderer, hX, hY, crossLength * 2.0f, thicknessPx, color, outline, outlinePx); - drawSegment(renderer, vX, vY, thicknessPx, crossLength * 2.0f, color, outline, outlinePx); - } - - private void updateSwapDetector(Player player) { - ItemStack current = player.getItemBySlot(EquipmentSlot.CHEST); - boolean isElytra = isElytra(current); - boolean isChestplate = isChestplate(current); - boolean candidate = isElytra || isChestplate; - - if (!chestInit) { - chestInit = true; - lastSwapCandidate = candidate; - lastChestWasElytra = isElytra; - lastChestStack = current.copy(); - return; - } - - if (candidate && lastSwapCandidate && isElytra != lastChestWasElytra - && !ItemStack.isSameItem(current, lastChestStack)) { - Itemizer.showElytraSwap(current.copy()); - } - - lastSwapCandidate = candidate; - lastChestWasElytra = isElytra; - lastChestStack = current.copy(); - } - - private boolean isElytra(ItemStack stack) { - return !stack.isEmpty() && stack.is(Items.ELYTRA); - } - - private boolean isChestplate(ItemStack stack) { - if (stack.isEmpty() || stack.is(Items.ELYTRA)) return false; - Equippable eq = stack.get(DataComponents.EQUIPPABLE); - return eq != null && eq.slot() == EquipmentSlot.CHEST; - } - - private void resetSwapDetector() { - chestInit = false; - lastSwapCandidate = false; - lastChestWasElytra = false; - lastChestStack = ItemStack.EMPTY; - } - - private void drawSegment(Renderer2D renderer, - double x, - double y, - double w, - double h, - int color, - boolean outline, - float outlinePx) { - if (outline) { - renderer.quad(x - outlinePx, y - outlinePx, w + outlinePx * 2.0f, h + outlinePx * 2.0f, OUTLINE_COLOR); - } - renderer.quad(x, y, w, h, color); - } - - private enum CrosshairType implements EnumValue.AliasProvider { - CLASSIC, - T_CLASSIC, - ORBIZ, - PINWHEEL, - DOT; - - @Override - public List aliases() { - return switch (this) { - case CLASSIC -> List.of("Classic"); - case T_CLASSIC -> List.of("T-Classic"); - case ORBIZ -> List.of("Orbiz"); - case PINWHEEL -> List.of("Pinwheel"); - case DOT -> List.of("Dot"); - }; - } - } - - private enum OrbizMotionMode implements EnumValue.AliasProvider { - STATIC, - DYNAMIC; - - @Override - public List aliases() { - return switch (this) { - case STATIC -> List.of("Static", "Off", "Disabled", "false"); - case DYNAMIC -> List.of("Dynamic", "On", "Enabled", "true"); - }; - } - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/BlockHighlight.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/BlockHighlight.java deleted file mode 100644 index 6b7549b..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/BlockHighlight.java +++ /dev/null @@ -1,692 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.features.module.modules.visuals.scene; - -import net.minecraft.client.Minecraft; -import net.minecraft.client.multiplayer.MultiPlayerGameMode; -import net.minecraft.core.BlockPos; -import net.minecraft.server.level.BlockDestructionProgress; -import net.minecraft.util.Mth; -import net.minecraft.world.entity.Entity; -import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.phys.AABB; -import net.minecraft.world.phys.BlockHitResult; -import net.minecraft.world.phys.HitResult; -import net.minecraft.world.phys.shapes.CollisionContext; -import net.minecraft.world.phys.shapes.VoxelShape; -import dev.loki.lovisual.config.common.CommonSettingSchemas; -import dev.loki.lovisual.config.values.primitive.BooleanValue; -import dev.loki.lovisual.config.values.mode.EnumValue; -import dev.loki.lovisual.config.values.primitive.NumberValue; -import dev.loki.lovisual.config.values.color.RGBAColorValue; -import dev.loki.lovisual.features.module.core.Module; -import dev.loki.lovisual.features.module.core.ModuleCategory; -import dev.loki.lovisual.features.module.core.ModuleInfo; -import dev.loki.lovisual.features.module.phase.WorldPhase; -import dev.loki.lovisual.mixins.accessors.misc.MultiPlayerGameModeAccessor; -import dev.loki.lovisual.render.engine.RenderState; -import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors; -import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; -import dev.loki.lovisual.render.engine.renderer.Renderer3D; -import dev.loki.lovisual.render.engine.uniform.MeshBuilder; -import dev.loki.lovisual.render.engine.uniform.impl.world.liquid.WaterHighlightUniforms; -import dev.loki.lovisual.util.time.GameClock; -import com.mojang.blaze3d.pipeline.RenderPipeline; - -//todo Description -@ModuleInfo( - id = "blockhighlight", - displayName = "BlockHighlight", - category = ModuleCategory.VISUALS -) -public class BlockHighlight extends Module { - - private static final double RAINBOW_SPATIAL_SCALE = 12.0; - private static final double GEOMETRY_EXPAND = 0.0025; - private final BooleanValue outlineEnabled = - bool( - "outline_enabled", - true - ); - private final EnumValue outlineMode = - visibleWhen(enumMode( - "outline_mode", - OutlineMode.LINES, - OutlineMode.values() - ), outlineEnabled::get); - private final RGBAColorValue outlineColor = - visibleWhen(color( - "outline_color", - "#66FFFFFF" - ), outlineEnabled::get); - private final EnumValue outlineColorMode = - visibleWhen(enumCommon( - "outline_color_mode", - "outline_color_mode", - CommonSettingSchemas.RENDER_COLOR_MODE, - ColorMode.STATIC, - ColorMode.values() - ), outlineEnabled::get); - private final RGBAColorValue outlineColor2 = - visibleWhen(color( - "outline_color2", - "#FFFFAA00" - ), this::usesSecondaryOutlineColor); - private final EnumValue outlineShaderStyle = - visibleWhen(enumMode( - "outline_shader_style", - ShaderStyle.WATER_CAUSTIC, - ShaderStyle.values() - ), this::usesShaderOutline); - private final NumberValue outlineColorSpeed = - visibleWhen(num( - "outline_color_speed", - 18, - 2, - 54 - ), outlineEnabled::get); - private final NumberValue outlineThickness = - visibleWhen(num( - "outline_thickness", - 1.0f, - 0.5f, - 4.0f - ), outlineEnabled::get); - private final BooleanValue outlineDepthTest = - visibleWhen(bool( - "outline_depth_test", - false - ), outlineEnabled::get); - private final BooleanValue breakingEnabled = - bool( - "breaking_enabled", - true - ); - private final BooleanValue breakingSelf = - visibleWhen(bool( - "breaking_self", - true - ), breakingEnabled::get); - private final BooleanValue breakingOtherPlayers = - visibleWhen(bool( - "breaking_other_players", - true - ), breakingEnabled::get); - private final EnumValue breakingAnimation = - visibleWhen(enumMode( - "breaking_animation", - BreakAnimation.GROW, - BreakAnimation.values() - ), breakingEnabled::get); - private final RGBAColorValue breakingColor = - visibleWhen(color( - "breaking_color", - "#66FFFFFF" - ), breakingEnabled::get); - private final RGBAColorValue breakingColor2 = - visibleWhen(color( - "breaking_color2", - "#22FFAA00" - ), breakingEnabled::get); - - private static void addGradientLine(MeshBuilder mesh, - double x1, double y1, double z1, - double x2, double y2, double z2, - int argb1, - int argb2) { - mesh.ensureLineCapacity(); - - putColorVertex(mesh, x1, y1, z1, argb1); - int i1 = mesh.next(); - - putColorVertex(mesh, x2, y2, z2, argb2); - int i2 = mesh.next(); - - mesh.line(i1, i2); - } - - private static void addGradientQuad(MeshBuilder mesh, - double x1, double y1, double z1, int c1, - double x2, double y2, double z2, int c2, - double x3, double y3, double z3, int c3, - double x4, double y4, double z4, int c4) { - mesh.ensureQuadCapacity(); - - putColorVertex(mesh, x1, y1, z1, c1); - int i1 = mesh.next(); - - putColorVertex(mesh, x2, y2, z2, c2); - int i2 = mesh.next(); - - putColorVertex(mesh, x3, y3, z3, c3); - int i3 = mesh.next(); - - putColorVertex(mesh, x4, y4, z4, c4); - int i4 = mesh.next(); - - mesh.quad(i1, i2, i3, i4); - } - - private static void putColorVertex(MeshBuilder mesh, double x, double y, double z, int argb) { - mesh.vec3(x, y, z).color(red(argb), green(argb), blue(argb), alpha(argb)); - } - - private static void addFilledBox(Renderer3D renderer, AABB box, int argb) { - int a = alpha(argb); - int r = red(argb); - int g = green(argb); - int b = blue(argb); - - double minX = box.minX; - double minY = box.minY; - double minZ = box.minZ; - double maxX = box.maxX; - double maxY = box.maxY; - double maxZ = box.maxZ; - - renderer.quad(minX, minY, minZ, maxX, minY, minZ, maxX, minY, maxZ, minX, minY, maxZ, r, g, b, a); - renderer.quad(minX, maxY, minZ, minX, maxY, maxZ, maxX, maxY, maxZ, maxX, maxY, minZ, r, g, b, a); - renderer.quad(minX, minY, maxZ, maxX, minY, maxZ, maxX, maxY, maxZ, minX, maxY, maxZ, r, g, b, a); - renderer.quad(minX, minY, minZ, minX, maxY, minZ, maxX, maxY, minZ, maxX, minY, minZ, r, g, b, a); - renderer.quad(maxX, minY, minZ, maxX, maxY, minZ, maxX, maxY, maxZ, maxX, minY, maxZ, r, g, b, a); - renderer.quad(minX, minY, minZ, minX, minY, maxZ, minX, maxY, maxZ, minX, maxY, minZ, r, g, b, a); - } - - private static float stageToProgress(int stage) { - if (stage < 0) return 0.0f; - return Mth.clamp(stage / 10.0f, 0.0f, 1.0f); - } - - private static int interpolateArgb(int from, int to, float amount) { - amount = Mth.clamp(amount, 0.0f, 1.0f); - - int a = interpolateChannel(alpha(from), alpha(to), amount); - int r = interpolateChannel(red(from), red(to), amount); - int g = interpolateChannel(green(from), green(to), amount); - int b = interpolateChannel(blue(from), blue(to), amount); - - return (a << 24) | (r << 16) | (g << 8) | b; - } - - private static int interpolateChannel(int from, int to, float amount) { - return from + Math.round((to - from) * amount); - } - - private static int withAlpha(int argb, int alpha) { - return (Mth.clamp(alpha, 0, 255) << 24) | (argb & 0x00FFFFFF); - } - - private static int red(int argb) { - return (argb >>> 16) & 255; - } - - private static int green(int argb) { - return (argb >>> 8) & 255; - } - - private static int blue(int argb) { - return argb & 255; - } - - private static int alpha(int argb) { - return (argb >>> 24) & 255; - } - - public boolean isOutlineActive() { - return isEnabled() && outlineEnabled.get(); - } - - @Override - public WorldPhase getWorldPhase() { - return WorldPhase.BEFORE_TRANSLUCENT; - } - - @Override - public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { - Minecraft mc = Minecraft.getInstance(); - if (!isEnabled() || mc.level == null || mc.player == null) return; - - if (outlineEnabled.get()) { - renderTargetOutline(renderer); - } - - if (breakingEnabled.get()) { - renderBreakingHighlight(renderer); - } - } - - private void renderTargetOutline(Renderer3D renderer) { - Minecraft mc = Minecraft.getInstance(); - HitResult hit = mc.hitResult; - if (!(hit instanceof BlockHitResult blockHit)) return; - if (hit.getType() == HitResult.Type.MISS) return; - - BlockPos pos = blockHit.getBlockPos(); - BlockState state = mc.level.getBlockState(pos); - if (state.isAir()) return; - - Entity cameraEntity = mc.getCameraEntity(); - CollisionContext context = cameraEntity != null ? CollisionContext.of(cameraEntity) : CollisionContext.empty(); - VoxelShape shape = state.getShape(mc.level, pos, context); - if (shape.isEmpty()) return; - - VoxelShape worldShape = shape.move(pos.getX(), pos.getY(), pos.getZ()); - OutlineMode mode = outlineMode.get(); - - if (mode == OutlineMode.BOX || mode == OutlineMode.BOTH) { - renderShapeBoxes(renderer, worldShape); - } - - if (mode == OutlineMode.LINES || mode == OutlineMode.BOTH) { - renderShapeLines(renderer, worldShape); - } - - if (mode == OutlineMode.SHADER) { - renderShapeShader(renderer, worldShape); - } - } - - private void renderShapeShader(Renderer3D renderer, VoxelShape worldShape) { - RenderPipeline pipeline = switch (outlineShaderStyle.get()) { - case SCAN_LINES -> LoVisualRenderPipelines.WORLD_BLOCK_HIGHLIGHT_SCANLINES; - case HOLOGRAM -> LoVisualRenderPipelines.WORLD_BLOCK_HIGHLIGHT_HOLOGRAM; - default -> LoVisualRenderPipelines.WORLD_BLOCK_HIGHLIGHT_WATERCAUSTIC; - }; - - int color = outlineColor.getArgb(); - int color2 = outlineColor2.getArgb(); - float time = GameClock.millis() / 1000f; - - WaterHighlightUniforms.update( - red(color) / 255f, green(color) / 255f, blue(color) / 255f, alpha(color) / 255f, - red(color2) / 255f, green(color2) / 255f, blue(color2) / 255f, alpha(color2) / 255f, - time - ); - - Renderer3D.BatchBindings bindings = Renderer3D.BatchBindings.none() - .withUniform("WaterData", WaterHighlightUniforms.get()); - MeshBuilder mesh = renderer.batch(pipeline, Renderer3D.DepthMode.NONE, bindings); - if (mesh == null) return; - - worldShape.forAllBoxes((minX, minY, minZ, maxX, maxY, maxZ) -> { - double x1 = minX - GEOMETRY_EXPAND; - double y1 = minY - GEOMETRY_EXPAND; - double z1 = minZ - GEOMETRY_EXPAND; - double x2 = maxX + GEOMETRY_EXPAND; - double y2 = maxY + GEOMETRY_EXPAND; - double z2 = maxZ + GEOMETRY_EXPAND; - - addTexturedBox(mesh, x1, y1, z1, x2, y2, z2); - }); - } - - private static void addTexturedBox(MeshBuilder mesh, - double x1, double y1, double z1, - double x2, double y2, double z2) { - addTexturedQuad(mesh, x1, y1, z1, x2, y1, z1, x2, y2, z1, x1, y2, z1); - addTexturedQuad(mesh, x2, y1, z2, x1, y1, z2, x1, y2, z2, x2, y2, z2); - addTexturedQuad(mesh, x1, y1, z2, x1, y1, z1, x1, y2, z1, x1, y2, z2); - addTexturedQuad(mesh, x2, y1, z1, x2, y1, z2, x2, y2, z2, x2, y2, z1); - addTexturedQuad(mesh, x1, y2, z1, x2, y2, z1, x2, y2, z2, x1, y2, z2); - addTexturedQuad(mesh, x1, y1, z2, x2, y1, z2, x2, y1, z1, x1, y1, z1); - } - - private static void addTexturedQuad(MeshBuilder mesh, - double x1, double y1, double z1, - double x2, double y2, double z2, - double x3, double y3, double z3, - double x4, double y4, double z4) { - mesh.ensureQuadCapacity(); - int i1 = mesh.vec3(x1, y1, z1).vec2(0, 0).color(255, 255, 255, 255).next(); - int i2 = mesh.vec3(x2, y2, z2).vec2(1, 0).color(255, 255, 255, 255).next(); - int i3 = mesh.vec3(x3, y3, z3).vec2(1, 1).color(255, 255, 255, 255).next(); - int i4 = mesh.vec3(x4, y4, z4).vec2(0, 1).color(255, 255, 255, 255).next(); - mesh.quad(i1, i2, i3, i4); - } - - private void renderShapeLines(Renderer3D renderer, VoxelShape worldShape) { - MeshBuilder mesh = renderer.batch( - outlineDepthTest.get() - ? LoVisualRenderPipelines.WORLD_COLORED_DEPTH - : LoVisualRenderPipelines.WORLD_COLORED, - outlineDepthTest.get() - ? Renderer3D.DepthMode.PRE_DEPTH - : Renderer3D.DepthMode.NONE - ); - - if (mesh == null) return; - - double overlap = outlineDepthTest.get() - ? Math.max(0.0015, outlineThickness.get() * 0.003) - : 0.0008; - - worldShape.forAllEdges((x1, y1, z1, x2, y2, z2) -> { - int c1 = getOutlineColor(x1, y1, z1); - int c2 = getOutlineColor(x2, y2, z2); - - addEdgeRibbon( - mesh, - x1, y1, z1, - x2, y2, z2, - overlap, - c1, - c2 - ); - }); - } - - private void addEdgeRibbon(MeshBuilder mesh, - double x1, double y1, double z1, - double x2, double y2, double z2, - double overlap, - int argb1, - int argb2) { - double mx = (x1 + x2) * 0.5; - double my = (y1 + y2) * 0.5; - double mz = (z1 + z2) * 0.5; - - double ex = x2 - x1; - double ey = y2 - y1; - double ez = z2 - z1; - double edgeLenSq = ex * ex + ey * ey + ez * ez; - if (edgeLenSq <= 1.0e-10) { - return; - } - - double toCamX = RenderState.cameraPos.x - mx; - double toCamY = RenderState.cameraPos.y - my; - double toCamZ = RenderState.cameraPos.z - mz; - double toCamLenSq = toCamX * toCamX + toCamY * toCamY + toCamZ * toCamZ; - if (toCamLenSq <= 1.0e-10) { - return; - } - - double sideX = ey * toCamZ - ez * toCamY; - double sideY = ez * toCamX - ex * toCamZ; - double sideZ = ex * toCamY - ey * toCamX; - double sideLenSq = sideX * sideX + sideY * sideY + sideZ * sideZ; - if (sideLenSq <= 1.0e-10) { - return; - } - - double sideInv = 1.0 / Math.sqrt(sideLenSq); - double halfWidth = computeRibbonHalfWidth(mx, my, mz); - - sideX *= sideInv * halfWidth; - sideY *= sideInv * halfWidth; - sideZ *= sideInv * halfWidth; - - double camInv = 1.0 / Math.sqrt(toCamLenSq); - double biasX = toCamX * camInv * overlap; - double biasY = toCamY * camInv * overlap; - double biasZ = toCamZ * camInv * overlap; - - addGradientQuad( - mesh, - x1 - sideX + biasX, y1 - sideY + biasY, z1 - sideZ + biasZ, argb1, - x1 + sideX + biasX, y1 + sideY + biasY, z1 + sideZ + biasZ, argb1, - x2 + sideX + biasX, y2 + sideY + biasY, z2 + sideZ + biasZ, argb2, - x2 - sideX + biasX, y2 - sideY + biasY, z2 - sideZ + biasZ, argb2 - ); - } - - private double computeRibbonHalfWidth(double x, double y, double z) { - double dx = RenderState.cameraPos.x - x; - double dy = RenderState.cameraPos.y - y; - double dz = RenderState.cameraPos.z - z; - double dist = Math.sqrt(dx * dx + dy * dy + dz * dz); - - return 0.003 + outlineThickness.get() * (0.0025 + dist * 0.00018); - } - - private void renderShapeBoxes(Renderer3D renderer, VoxelShape worldShape) { - MeshBuilder mesh = renderer.batch( - outlineDepthTest.get() - ? LoVisualRenderPipelines.WORLD_COLORED_DEPTH - : LoVisualRenderPipelines.WORLD_COLORED, - outlineDepthTest.get() - ? Renderer3D.DepthMode.PRE_DEPTH - : Renderer3D.DepthMode.NONE - ); - - if (mesh == null) return; - - worldShape.forAllBoxes((minX, minY, minZ, maxX, maxY, maxZ) -> { - double x1 = minX - GEOMETRY_EXPAND; - double y1 = minY - GEOMETRY_EXPAND; - double z1 = minZ - GEOMETRY_EXPAND; - double x2 = maxX + GEOMETRY_EXPAND; - double y2 = maxY + GEOMETRY_EXPAND; - double z2 = maxZ + GEOMETRY_EXPAND; - - addGradientBox(mesh, x1, y1, z1, x2, y2, z2); - }); - } - - private void addGradientBox(MeshBuilder mesh, - double x1, double y1, double z1, - double x2, double y2, double z2) { - int c000 = getOutlineColor(x1, y1, z1); - int c001 = getOutlineColor(x1, y1, z2); - int c010 = getOutlineColor(x1, y2, z1); - int c011 = getOutlineColor(x1, y2, z2); - int c100 = getOutlineColor(x2, y1, z1); - int c101 = getOutlineColor(x2, y1, z2); - int c110 = getOutlineColor(x2, y2, z1); - int c111 = getOutlineColor(x2, y2, z2); - - addGradientQuad(mesh, x1, y1, z1, c000, x2, y1, z1, c100, x2, y2, z1, c110, x1, y2, z1, c010); - addGradientQuad(mesh, x2, y1, z2, c101, x1, y1, z2, c001, x1, y2, z2, c011, x2, y2, z2, c111); - addGradientQuad(mesh, x1, y1, z2, c001, x1, y1, z1, c000, x1, y2, z1, c010, x1, y2, z2, c011); - addGradientQuad(mesh, x2, y1, z1, c100, x2, y1, z2, c101, x2, y2, z2, c111, x2, y2, z1, c110); - addGradientQuad(mesh, x1, y2, z1, c010, x2, y2, z1, c110, x2, y2, z2, c111, x1, y2, z2, c011); - addGradientQuad(mesh, x1, y1, z2, c001, x2, y1, z2, c101, x2, y1, z1, c100, x1, y1, z1, c000); - } - - private void renderBreakingHighlight(Renderer3D renderer) { - Minecraft mc = Minecraft.getInstance(); - if (breakingSelf.get()) { - renderSelfBreaking(renderer); - } - - if (breakingOtherPlayers.get() && mc.level != null) { - var destructionProgress = mc.level.destructionProgress(); - if (destructionProgress != null && !destructionProgress.isEmpty()) { - destructionProgress.forEach((packedPos, infos) -> { - if (infos == null || infos.isEmpty()) { - return; - } - - BlockDestructionProgress info = infos.last(); - if (mc.player != null && info.getId() == mc.player.getId()) { - return; - } - - renderBreakingInfo(renderer, info.getPos(), stageToProgress(info.getProgress())); - }); - } - } - } - - private void renderSelfBreaking(Renderer3D renderer) { - Minecraft mc = Minecraft.getInstance(); - MultiPlayerGameMode interactionManager = mc.gameMode; - if (interactionManager == null || !interactionManager.isDestroying()) return; - if (!(interactionManager instanceof MultiPlayerGameModeAccessor accessor)) return; - - BlockPos pos = accessor.lovisual$getCurrentBreakingPos(); - if (pos == null) return; - - float progress = Mth.clamp(accessor.lovisual$getCurrentBreakingProgress(), 0.0f, 1.0f); - renderBreakingInfo(renderer, pos, progress); - } - - private void renderBreakingInfo(Renderer3D renderer, BlockPos pos, float progress) { - Minecraft mc = Minecraft.getInstance(); - if (mc.level == null || pos == null) return; - - BlockState state = mc.level.getBlockState(pos); - if (state.isAir()) return; - - VoxelShape shape = state.getShape(mc.level, pos, CollisionContext.empty()); - if (shape.isEmpty()) return; - - float clamped = Mth.clamp(progress, 0.0f, 1.0f); - int argb = interpolateArgb(breakingColor.getArgb(), breakingColor2.getArgb(), clamped); - - shape.move(pos.getX(), pos.getY(), pos.getZ()).forAllBoxes((minX, minY, minZ, maxX, maxY, maxZ) -> { - AABB box = animateBox(new AABB(minX, minY, minZ, maxX, maxY, maxZ), clamped); - if (isRenderable(box)) { - addFilledBox(renderer, box, argb); - } - }); - } - - private AABB animateBox(AABB box, float progress) { - double scale = switch (breakingAnimation.get()) { - case GROW -> progress; - case SHRINK -> 1.0 - progress; - case STATIC -> 1.0; - }; - - scale = Mth.clamp((float) scale, 0.0f, 1.0f); - if (scale >= 0.999f) { - return box; - } - - double cx = (box.minX + box.maxX) * 0.5; - double cy = (box.minY + box.maxY) * 0.5; - double cz = (box.minZ + box.maxZ) * 0.5; - double hx = (box.maxX - box.minX) * 0.5 * scale; - double hy = (box.maxY - box.minY) * 0.5 * scale; - double hz = (box.maxZ - box.minZ) * 0.5 * scale; - - return new AABB(cx - hx, cy - hy, cz - hz, cx + hx, cy + hy, cz + hz); - } - - private boolean isRenderable(AABB box) { - return box.maxX - box.minX > 1.0e-4 - && box.maxY - box.minY > 1.0e-4 - && box.maxZ - box.minZ > 1.0e-4; - } - - private int getOutlineColor(double x, double y, double z) { - int base = outlineColor.getArgb(); - return AnimatedRenderColors.resolve( - animatedOutlineColorMode(), - outlineColorSpeed.get(), - spatialIndex(x, y, z), - base, - outlineColor2.getArgb(), - true - ); - } - - private int spatialIndex(double x, double y, double z) { - int xs = (int) Math.round(x * (RAINBOW_SPATIAL_SCALE + 1.0)); - int ys = (int) Math.round(y * (RAINBOW_SPATIAL_SCALE - 2.0)); - int zs = (int) Math.round(z * (RAINBOW_SPATIAL_SCALE + 3.0)); - return Math.floorMod(xs + ys + zs, 3600); - } - - private boolean usesSecondaryOutlineColor() { - if (!outlineEnabled.get()) return false; - return AnimatedRenderColors.usesSecondary(animatedOutlineColorMode()); - } - - private boolean usesShaderOutline() { - return outlineEnabled.get() && outlineMode.get() == OutlineMode.SHADER; - } - - private AnimatedRenderColors.Mode animatedOutlineColorMode() { - return switch (outlineColorMode.get()) { - case RAINBOW -> AnimatedRenderColors.Mode.RAINBOW; - case LIGHT_RAINBOW -> AnimatedRenderColors.Mode.LIGHT_RAINBOW; - case SKY -> AnimatedRenderColors.Mode.SKY; - case FADE -> AnimatedRenderColors.Mode.FADE; - case DOUBLE_COLOR -> AnimatedRenderColors.Mode.DOUBLE_COLOR; - case ANALOGOUS -> AnimatedRenderColors.Mode.ANALOGOUS; - case THEME -> AnimatedRenderColors.Mode.THEME; - case STATIC -> AnimatedRenderColors.Mode.STATIC; - }; - } - - public enum ColorMode implements EnumValue.IdProvider { - STATIC("static"), - RAINBOW("rainbow"), - LIGHT_RAINBOW("light_rainbow"), - SKY("sky"), - FADE("fade"), - DOUBLE_COLOR("double_color"), - ANALOGOUS("analogous"), - THEME("theme"); - - private final String id; - - ColorMode(String id) { - this.id = id; - } - - @Override - public String id() { - return id; - } - } - - public enum OutlineMode implements EnumValue.IdProvider { - LINES("lines"), - BOX("box"), - BOTH("both"), - SHADER("shader"); - - private final String id; - - OutlineMode(String id) { - this.id = id; - } - - @Override - public String id() { - return id; - } - } - - public enum ShaderStyle implements EnumValue.IdProvider { - WATER_CAUSTIC("water_caustic"), - SCAN_LINES("scan_lines"), - HOLOGRAM("hologram"); - - private final String id; - - ShaderStyle(String id) { - this.id = id; - } - - @Override - public String id() { - return id; - } - } - - public enum BreakAnimation implements EnumValue.IdProvider { - GROW("grow"), - SHRINK("shrink"), - STATIC("static"); - - private final String id; - - BreakAnimation(String id) { - this.id = id; - } - - @Override - public String id() { - return id; - } - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/Chams.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/Chams.java deleted file mode 100644 index b210921..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/Chams.java +++ /dev/null @@ -1,985 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.features.module.modules.visuals.scene.visual; - -import dev.loki.lovisual.features.theme.Theme; -import com.mojang.blaze3d.GpuFormat; -import com.mojang.blaze3d.pipeline.RenderPipeline; -import com.mojang.blaze3d.pipeline.TextureTarget; -import com.mojang.blaze3d.systems.RenderSystem; -import com.mojang.blaze3d.textures.AddressMode; -import com.mojang.blaze3d.textures.FilterMode; -import com.mojang.blaze3d.textures.GpuSampler; -import com.mojang.blaze3d.textures.GpuTextureView; -import com.mojang.blaze3d.vertex.PoseStack; -import dev.loki.lovisual.config.values.color.RGBAColorValue; -import dev.loki.lovisual.config.values.color.RGBColorValue; -import dev.loki.lovisual.config.values.mode.ModeValue; -import dev.loki.lovisual.config.values.primitive.BooleanValue; -import dev.loki.lovisual.config.values.primitive.NumberValue; -import net.irisshaders.iris.mixinterface.ItemInHandInterface; -import net.irisshaders.iris.pathways.HandRenderer; -import net.minecraft.client.Minecraft; -import net.minecraft.client.renderer.GameRenderer; -import net.minecraft.client.renderer.RenderBuffers; -import net.minecraft.client.renderer.SubmitNodeStorage; -import net.minecraft.client.renderer.feature.FeatureRenderDispatcher; -import net.minecraft.client.renderer.state.level.CameraRenderState; -import net.minecraft.util.Util; -import org.joml.Matrix4fc; -import dev.loki.lovisual.features.theme.Themes; -import dev.loki.lovisual.features.module.core.Module; -import dev.loki.lovisual.features.module.core.ModuleCategory; -import dev.loki.lovisual.features.module.core.ModuleInfo; -import dev.loki.lovisual.features.module.lifecycle.Modules; -import dev.loki.lovisual.mixins.accessors.renderer.GameRendererAccessor; -import dev.loki.lovisual.mixins.iris.hand.IrisHandRendererAccessor; -import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; -import dev.loki.lovisual.render.engine.postprocess.post.PostProcessManager; -import dev.loki.lovisual.render.engine.postprocess.post.PostProcessPass; -import dev.loki.lovisual.render.engine.renderer.FullScreenRenderer; -import dev.loki.lovisual.render.engine.uniform.impl.hand.HandBlendUniforms; -import dev.loki.lovisual.render.engine.uniform.impl.hand.HandChamsUniforms; -import dev.loki.lovisual.render.engine.uniform.impl.hand.HandGlassUniforms; -import dev.loki.lovisual.render.engine.uniform.impl.hand.HandMetallicUniforms; -import dev.loki.lovisual.render.engine.uniform.impl.hand.effects.HandNoiseUniforms; -import dev.loki.lovisual.render.engine.uniform.impl.hand.effects.HandSmokeUniforms; -import dev.loki.lovisual.render.iris.state.IrisHandMaskState; -import dev.loki.lovisual.render.iris.runtime.IrisRuntime; -import dev.loki.lovisual.render.engine.RenderState; -import dev.loki.lovisual.features.module.modules.visuals.camera.Freecam; - -//todo Description -@ModuleInfo(id = "chams", displayName = "Chams", category = ModuleCategory.VISUALS) -public class Chams extends Module { - - private static final String SETTING_HANDS = "hands"; - private static final String SETTING_MODE = "mode"; - private static final String SETTING_FILL = "fill"; - private static final String SETTING_VISUAL_COLOR_SOURCE = "visual_color_source"; - private static final String SETTING_EFFECT_COLOR_SOURCE = "effect_color_source"; - private static final String SETTING_GLOW = "glow"; - private static final String SETTING_GLOW_COLOR = "glow_color"; - private static final String SETTING_GLOW_ALPHA = "glow_alpha"; - private static final String SETTING_GLOW_STRENGTH = "glow_strength"; - private static final String SETTING_SHADOW = "shadow"; - private static final String SETTING_SHADOW_COLOR = "shadow_color"; - private static final String SETTING_SHADOW_ALPHA = "shadow_alpha"; - private static final String SETTING_SHADOW_STRENGTH = "shadow_strength"; - private static final String SETTING_EDGE_WIDTH = "edge_width"; - private static final String SETTING_QUALITY = "quality"; - private static final String SETTING_SMOKE_OCTAVES = "smoke_octaves"; - private static final String SETTING_SMOKE_SPEED = "smoke_speed"; - private static final String SETTING_SMOKE_SCALE = "smoke_scale"; - private static final String SETTING_SMOKE_CONTRAST = "smoke_contrast"; - private static final String SETTING_SMOKE_SWIRL = "smoke_swirl"; - private static final String SETTING_SMOKE_DENSITY = "smoke_density"; - private static final String SETTING_NOISE_ALPHA = "noise_alpha"; - private static final String SETTING_METALLIC_BASE = "metallic_base"; - private static final String SETTING_METALLIC_HIGHLIGHT = "metallic_highlight"; - private static final String SETTING_METALLIC_INTENSITY = "metallic_intensity"; - private static final String SETTING_METALLIC_SHARPNESS = "metallic_sharpness"; - private static final String SETTING_METALLIC_EDGE = "metallic_edge"; - private static final String SETTING_METALLIC_SWEEP_SPEED = "metallic_sweep_speed"; - private static final String SETTING_METALLIC_SWEEP_SCALE = "metallic_sweep_scale"; - private static final String SETTING_METALLIC_BRUSHED_LINES = "metallic_brushed_lines"; - private static final String SETTING_METALLIC_FLAKES = "metallic_flakes"; - private static final String SETTING_METALLIC_PRISM = "metallic_prism"; - private static final String SETTING_DUAL_SHADER = "dual_shader"; - private static final String SETTING_SHADER_1 = "shader1"; - private static final String SETTING_SHADER_2 = "shader2"; - private static final String SETTING_BLEND_MODE = "blend_mode"; - private static final String SETTING_BLEND_INTENSITY = "blend_intensity"; - private static final String SETTING_SHADER_1_STRENGTH = "shader1_strength"; - private static final String SETTING_SHADER_2_STRENGTH = "shader2_strength"; - private static final String SETTING_SNOW_COLOR = "snow_color"; - private static final String SETTING_SNOW_INTENSITY = "snow_intensity"; - private static final String SETTING_SNOW_SPEED = "snow_speed"; - private static final String SETTING_CHROMA_SPEED = "chroma_speed"; - private static final String SETTING_CHROMA_SATURATION = "chroma_saturation"; - private static final String SETTING_CHROMA_BRIGHTNESS = "chroma_brightness"; - private static final String SETTING_SOLID_COLOR1 = "solid_color1"; - private static final String SETTING_SOLID_COLOR2 = "solid_color2"; - private static final String SETTING_SOLID_SPEED = "solid_speed"; - private static final String SETTING_STRIPES_COLOR1 = "stripes_color1"; - private static final String SETTING_STRIPES_COLOR2 = "stripes_color2"; - private static final String SETTING_STRIPES_WIDTH = "stripes_width"; - private static final String SETTING_STRIPES_SPEED = "stripes_speed"; - private static final String SETTING_GLOW_SHADER_FILL = "glow_shader_fill"; - private static final String SETTING_GLOW_SHADER_RADIUS = "glow_shader_radius"; - private static final String SETTING_GLOW_SHADER_POWER = "glow_shader_power"; - private static final String SETTING_GLOW_SHADER_DISPERSION = "glow_shader_dispersion"; - private static final String SETTING_GLOW_SHADER_COLOR = "glow_shader_color"; - private static final String SETTING_BALATRO_COLOR1 = "balatro_color1"; - private static final String SETTING_BALATRO_COLOR2 = "balatro_color2"; - private static final String SETTING_BALATRO_COLOR3 = "balatro_color3"; - private static final String SETTING_BALATRO_CONTRAST = "balatro_contrast"; - private static final String SETTING_BALATRO_LIGHTING = "balatro_lighting"; - private static final String SETTING_HIDE_HAND = "hide_hand"; - // Noise mode tuning; intentionally fixed so the mode stays zero-config besides color/alpha. - private static final float NOISE_OCTAVES = 3.0f; - private static final float NOISE_SCALE = 3.4f; - private static final float NOISE_SPEED = 0.85f; - private static final float NOISE_CONTRAST = 1.15f; - private static final float NOISE_BREATHE = 0.55f; - private static final float NOISE_DENSITY = 1.0f; - private final ModeValue mode = - modeSetting("handChamsMode", SETTING_MODE, "Smoke", "Smoke", "Metallic", "Glass", "Noise", "Snow", "Chroma", "Solid", "Stripes", "Glow", "Balatro", "Invert"); - - private final BooleanValue hands = - bool("chamsHands", SETTING_HANDS, true); - - private final RGBAColorValue fillColor = - color("handChamsFillColor", SETTING_FILL, "#66FFFFFF"); - - private final ModeValue visualColorSource = - visibleWhen(modeSetting("handChamsVisualColorSource", SETTING_VISUAL_COLOR_SOURCE, "Custom", "Custom", "Theme"), this::isHandEffectMode); - private final RGBColorValue metallicBase = - visibleWhen(colorNoAlpha("handChamsMetallicBase", SETTING_METALLIC_BASE, "#B0B4BA"), this::isCustomMetallicVisualColor); - private final RGBColorValue metallicHighlight = - visibleWhen(colorNoAlpha("handChamsMetallicHighlight", SETTING_METALLIC_HIGHLIGHT, "#FFFFFF"), this::isCustomMetallicVisualColor); - private final ModeValue effectColorSource = - visibleWhen(modeSetting("handChamsEffectColorSource", SETTING_EFFECT_COLOR_SOURCE, "Theme", "Material", "Theme", "Custom"), this::isHandEffectMode); - private final RGBColorValue glowColor = - visibleWhen(colorNoAlpha("handChamsGlowColor", SETTING_GLOW_COLOR, "#87DFFF"), this::isCustomEffectColor); - private final RGBColorValue shadowColor = - visibleWhen(colorNoAlpha("handChamsShadowColor", SETTING_SHADOW_COLOR, "#10131A"), this::isCustomEffectColor); - private final BooleanValue glow = - visibleWhen(bool("handChamsGlow", SETTING_GLOW, true), this::isHandEffectMode); - private final NumberValue glowAlpha = - visibleWhen(num("handChamsGlowAlpha", SETTING_GLOW_ALPHA, 0.72f, 0.0f, 1.0f), this::isGlowVisible); - private final NumberValue glowStrength = - visibleWhen(num("handChamsGlowStrength", SETTING_GLOW_STRENGTH, 1.15f, 0.0f, 6.0f), this::isGlowVisible); - private final BooleanValue shadow = - visibleWhen(bool("handChamsShadow", SETTING_SHADOW, true), this::isHandEffectMode); - private final NumberValue shadowAlpha = - visibleWhen(num("handChamsShadowAlpha", SETTING_SHADOW_ALPHA, 0.48f, 0.0f, 1.0f), this::isShadowVisible); - private final NumberValue shadowStrength = - visibleWhen(num("handChamsShadowStrength", SETTING_SHADOW_STRENGTH, 1.0f, 0.0f, 4.0f), this::isShadowVisible); - private final NumberValue edgeWidth = - visibleWhen(num("handChamsEdgeWidth", SETTING_EDGE_WIDTH, 10.0f, 0.0f, 36.0f), this::isHandEffectMode); - private final NumberValue quality = - visibleWhen(num("handChamsQuality", SETTING_QUALITY, 2, 1, 4), this::isHandEffectMode); - private final NumberValue smokeOctaves = - visibleWhen(num("handChamsSmokeOctaves", SETTING_SMOKE_OCTAVES, 4, 1, 6), this::isSmokeMode); - private final NumberValue smokeSpeed = - visibleWhen(num("chamsSmokeSpeed", SETTING_SMOKE_SPEED, 0.72f, 0.0f, 3.0f), this::isSmokeMode); - private final NumberValue smokeScale = - visibleWhen(num("handChamsSmokeScale", SETTING_SMOKE_SCALE, 4.4f, 1.0f, 12.0f), this::isSmokeMode); - private final NumberValue smokeContrast = - visibleWhen(num("handChamsSmokeContrast", SETTING_SMOKE_CONTRAST, 1.12f, 0.35f, 2.4f), this::isSmokeMode); - private final NumberValue smokeSwirl = - visibleWhen(num("handChamsSmokeSwirl", SETTING_SMOKE_SWIRL, 0.85f, 0.0f, 2.5f), this::isSmokeMode); - private final NumberValue smokeDensity = - visibleWhen(num("handChamsSmokeDensity", SETTING_SMOKE_DENSITY, 1.0f, 0.2f, 2.5f), this::isSmokeMode); - private final NumberValue noiseAlpha = - visibleWhen(num("handChamsNoiseAlpha", SETTING_NOISE_ALPHA, 0.85f, 0.0f, 1.0f), this::isNoiseMode); - private final NumberValue metallicIntensity = - visibleWhen(num("handChamsMetallicIntensity", SETTING_METALLIC_INTENSITY, 1.35f, 0.0f, 5.0f), this::isMetallicMode); - private final NumberValue metallicSharpness = - visibleWhen(num("handChamsMetallicSharpness", SETTING_METALLIC_SHARPNESS, 2.2f, 0.35f, 10.0f), this::isMetallicMode); - private final NumberValue metallicEdge = - visibleWhen(num("handChamsMetallicEdge", SETTING_METALLIC_EDGE, 1.25f, 0.0f, 4.0f), this::isMetallicMode); - private final NumberValue metallicSweepSpeed = - visibleWhen(num("handChamsMetallicSweepSpeed", SETTING_METALLIC_SWEEP_SPEED, 1.15f, 0.0f, 5.0f), this::isMetallicMode); - private final NumberValue metallicSweepScale = - visibleWhen(num("handChamsMetallicSweepScale", SETTING_METALLIC_SWEEP_SCALE, 10.0f, 1.0f, 36.0f), this::isMetallicMode); - private final NumberValue metallicBrushedLines = - visibleWhen(num("handChamsMetallicBrushedLines", SETTING_METALLIC_BRUSHED_LINES, 0.55f, 0.0f, 1.0f), this::isMetallicMode); - private final NumberValue metallicFlakes = - visibleWhen(num("handChamsMetallicFlakes", SETTING_METALLIC_FLAKES, 0.25f, 0.0f, 1.0f), this::isMetallicMode); - private final NumberValue metallicPrism = - visibleWhen(num("handChamsMetallicPrism", SETTING_METALLIC_PRISM, 0.18f, 0.0f, 1.0f), this::isMetallicMode); - private final BooleanValue dualShader = - bool("chamsDualShader", SETTING_DUAL_SHADER, false); - private final ModeValue shader1 = - visibleWhen(modeSetting("chamsShader1", SETTING_SHADER_1, "Glow", "Snow", "Chroma", "Solid", "Stripes", "Glow", "Balatro", "Invert"), dualShader::get); - private final ModeValue shader2 = - visibleWhen(modeSetting("chamsShader2", SETTING_SHADER_2, "Chroma", "Snow", "Chroma", "Solid", "Stripes", "Glow", "Balatro", "Invert"), dualShader::get); - private final ModeValue blendMode = - visibleWhen(modeSetting("chamsBlendMode", SETTING_BLEND_MODE, "Add", "Mix", "Add", "Multiply", "Screen", "Overlay", "Difference"), dualShader::get); - private final NumberValue blendIntensity = - visibleWhen(num("chamsBlendIntensity", SETTING_BLEND_INTENSITY, 1.0f, 0.0f, 1.0f), dualShader::get); - private final NumberValue shader1Strength = - visibleWhen(num("chamsShader1Strength", SETTING_SHADER_1_STRENGTH, 1.0f, 0.0f, 1.0f), dualShader::get); - private final NumberValue shader2Strength = - visibleWhen(num("chamsShader2Strength", SETTING_SHADER_2_STRENGTH, 1.0f, 0.0f, 1.0f), dualShader::get); - private final RGBColorValue snowColor = - visibleWhen(colorNoAlpha("chamsSnowColor", SETTING_SNOW_COLOR, "#FFFFFF"), this::isSnowMode); - private final NumberValue snowIntensity = - visibleWhen(num("chamsSnowIntensity", SETTING_SNOW_INTENSITY, 1.0f, 0.1f, 3.0f), this::isSnowMode); - private final NumberValue snowSpeed = - visibleWhen(num("chamsSnowSpeed", SETTING_SNOW_SPEED, 1.0f, 0.1f, 2.0f), this::isSnowMode); - private final NumberValue chromaSpeed = - visibleWhen(num("chamsChromaSpeed", SETTING_CHROMA_SPEED, 0.3f, 0.05f, 1.0f), this::isChromaMode); - private final NumberValue chromaSaturation = - visibleWhen(num("chamsChromaSaturation", SETTING_CHROMA_SATURATION, 0.5f, 0.1f, 1.0f), this::isChromaMode); - private final NumberValue chromaBrightness = - visibleWhen(num("chamsChromaBrightness", SETTING_CHROMA_BRIGHTNESS, 1.0f, 0.5f, 1.5f), this::isChromaMode); - private final RGBColorValue solidColor1 = - visibleWhen(colorNoAlpha("chamsSolidColor1", SETTING_SOLID_COLOR1, "#E91E63"), this::isSolidMode); - private final RGBColorValue solidColor2 = - visibleWhen(colorNoAlpha("chamsSolidColor2", SETTING_SOLID_COLOR2, "#9B59B6"), this::isSolidMode); - private final NumberValue solidSpeed = - visibleWhen(num("chamsSolidSpeed", SETTING_SOLID_SPEED, 0.5f, 0.1f, 1.0f), this::isSolidMode); - private final RGBColorValue stripesColor1 = - visibleWhen(colorNoAlpha("chamsStripesColor1", SETTING_STRIPES_COLOR1, "#778889"), this::isStripesMode); - private final RGBColorValue stripesColor2 = - visibleWhen(colorNoAlpha("chamsStripesColor2", SETTING_STRIPES_COLOR2, "#5A6566"), this::isStripesMode); - private final NumberValue stripesWidth = - visibleWhen(num("chamsStripesWidth", SETTING_STRIPES_WIDTH, 0.3f, 0.05f, 0.5f), this::isStripesMode); - private final NumberValue stripesSpeed = - visibleWhen(num("chamsStripesSpeed", SETTING_STRIPES_SPEED, 0.3f, 0.1f, 2.0f), this::isStripesMode); - private final BooleanValue glowShaderFill = - visibleWhen(bool("chamsGlowShaderFill", SETTING_GLOW_SHADER_FILL, true), this::isGlowShaderMode); - private final NumberValue glowShaderRadius = - visibleWhen(num("chamsGlowShaderRadius", SETTING_GLOW_SHADER_RADIUS, 0.04f, 0.01f, 0.2f), this::isGlowShaderMode); - private final NumberValue glowShaderPower = - visibleWhen(num("chamsGlowShaderPower", SETTING_GLOW_SHADER_POWER, 0.75f, 0.1f, 2.0f), this::isGlowShaderMode); - private final NumberValue glowShaderDispersion = - visibleWhen(num("chamsGlowShaderDispersion", SETTING_GLOW_SHADER_DISPERSION, 5.0f, 1.0f, 10.0f), this::isGlowShaderMode); - private final RGBColorValue glowShaderColor = - visibleWhen(colorNoAlpha("chamsGlowShaderColor", SETTING_GLOW_SHADER_COLOR, "#3498DB"), this::isGlowShaderMode); - private final RGBColorValue balatroColor1 = - visibleWhen(colorNoAlpha("chamsBalatroColor1", SETTING_BALATRO_COLOR1, "#DE443B"), this::isBalatroMode); - private final RGBColorValue balatroColor2 = - visibleWhen(colorNoAlpha("chamsBalatroColor2", SETTING_BALATRO_COLOR2, "#006BB4"), this::isBalatroMode); - private final RGBColorValue balatroColor3 = - visibleWhen(colorNoAlpha("chamsBalatroColor3", SETTING_BALATRO_COLOR3, "#162325"), this::isBalatroMode); - private final NumberValue balatroContrast = - visibleWhen(num("chamsBalatroContrast", SETTING_BALATRO_CONTRAST, 0.6f, 0.1f, 1.0f), this::isBalatroMode); - private final NumberValue balatroLighting = - visibleWhen(num("chamsBalatroLighting", SETTING_BALATRO_LIGHTING, 0.6f, 0.0f, 1.0f), this::isBalatroMode); - private final BooleanValue hideHand = - bool("chamsHideHand", SETTING_HIDE_HAND, false); - private final PostProcessPass handsPass = new HandsPass(); - private TextureTarget handMask; - private TextureTarget dualA; - private TextureTarget dualB; - private GpuSampler handMaskSampler; - private int bufferW = -1; - private int bufferH = -1; - private boolean maskReady; - private RenderBuffers irisHandRenderBuffers; - private FeatureRenderDispatcher irisHandFeatureDispatcher; - - { - PostProcessManager.register(handsPass); - } - - private static float channel(int argb, int shift) { - return ((argb >> shift) & 0xFF) / 255.0f; - } - - private static float clamp01(float v) { - if (v < 0.0f) return 0.0f; - if (v > 1.0f) return 1.0f; - return v; - } - - private static int forceOpaque(int argb) { - return 0xFF000000 | (argb & 0x00FFFFFF); - } - - private static int mixRgb(int a, int b, float t) { - t = clamp01(t); - int ar = (a >>> 16) & 0xFF; - int ag = (a >>> 8) & 0xFF; - int ab = a & 0xFF; - int br = (b >>> 16) & 0xFF; - int bg = (b >>> 8) & 0xFF; - int bb = b & 0xFF; - int rr = Math.round(ar + (br - ar) * t); - int rg = Math.round(ag + (bg - ag) * t); - int rb = Math.round(ab + (bb - ab) * t); - return 0xFF000000 | (rr << 16) | (rg << 8) | rb; - } - - public boolean isActive() { - Minecraft mc = Minecraft.getInstance(); - return isEnabled() && mc.player != null && mc.level != null; - } - - @Override - public void onDisable() { - maskReady = false; - closeStandaloneHandRenderer(); - } - - private FeatureRenderDispatcher getStandaloneHandFeatureDispatcher() { - Minecraft mc = Minecraft.getInstance(); - if (irisHandFeatureDispatcher != null) { - return irisHandFeatureDispatcher; - } - if (mc.gameRenderer == null || mc.getModelManager() == null || mc.getAtlasManager() == null || mc.font == null) { - return null; - } - irisHandRenderBuffers = new RenderBuffers(1); - irisHandFeatureDispatcher = new FeatureRenderDispatcher( - irisHandRenderBuffers, - mc.getModelManager(), - mc.getAtlasManager(), - mc.font, - mc.gameRenderer.gameRenderState() - ); - return irisHandFeatureDispatcher; - } - - private void endStandaloneHandFrame() { - if (irisHandRenderBuffers != null) { - irisHandRenderBuffers.endFrame(); - } - } - - private void closeStandaloneHandRenderer() { - if (irisHandFeatureDispatcher != null) { - irisHandFeatureDispatcher.close(); - irisHandFeatureDispatcher = null; - } - if (irisHandRenderBuffers != null) { - irisHandRenderBuffers.close(); - irisHandRenderBuffers = null; - } - } - - public boolean renderHandMask(GameRenderer renderer, CameraRenderState cameraRenderState, float tickDelta, Matrix4fc positionMatrix) { - Minecraft mc = Minecraft.getInstance(); - if (!isEnabled() || mc.player == null || mc.level == null) return false; - if (mc.gameMode == null) return false; - if (!hands.get() || !shouldRenderHand()) return false; - if (!mc.options.getCameraType().isFirstPerson()) return false; - - maskReady = false; - ensureBuffers(); - if (handMask == null) return false; - - if (mc.gameRenderer.mainRenderTarget() == null) return false; - - var colorTex = handMask.getColorTexture(); - if (colorTex == null) return false; - - var encoder = RenderSystem.getDevice().createCommandEncoder(); - encoder.clearColorTexture(colorTex, new org.joml.Vector4f(0.0f, 0.0f, 0.0f, 0.0f)); - - var depthTex = handMask.getDepthTexture(); - if (depthTex != null) { - encoder.clearDepthTexture(depthTex, 0.0); - } - - GpuTextureView prevColor = RenderSystem.outputColorTextureOverride; - GpuTextureView prevDepth = RenderSystem.outputDepthTextureOverride; - boolean prevRendering3D = RenderState.rendering3D; - - RenderSystem.outputColorTextureOverride = handMask.getColorTextureView(); - RenderSystem.outputDepthTextureOverride = handMask.getDepthTextureView(); - RenderState.rendering3D = true; - - try { - ((GameRendererAccessor) renderer).invokeRenderHand(cameraRenderState, tickDelta, positionMatrix); - } finally { - RenderState.rendering3D = prevRendering3D; - RenderSystem.outputColorTextureOverride = prevColor; - RenderSystem.outputDepthTextureOverride = prevDepth; - } - - maskReady = true; - return true; - } - - public boolean renderPreparedHandScene(FeatureRenderDispatcher dispatcher, SubmitNodeStorage storage) { - Minecraft mc = Minecraft.getInstance(); - if (!isEnabled() || mc.player == null || mc.level == null) return false; - if (dispatcher == null || storage == null) return false; - if (!hands.get() || !shouldRenderHand()) return false; - if (!mc.options.getCameraType().isFirstPerson()) return false; - - maskReady = false; - ensureBuffers(); - if (handMask == null || handMask.getColorTexture() == null) return false; - - var encoder = RenderSystem.getDevice().createCommandEncoder(); - encoder.clearColorTexture(handMask.getColorTexture(), new org.joml.Vector4f(0.0f, 0.0f, 0.0f, 0.0f)); - if (handMask.getDepthTexture() != null) { - encoder.clearDepthTexture(handMask.getDepthTexture(), 0.0); - } - - try (FeatureRenderDispatcher.PreparedFrame prepared = dispatcher.prepareFrame(storage)) { - GpuTextureView prevColor = RenderSystem.outputColorTextureOverride; - GpuTextureView prevDepth = RenderSystem.outputDepthTextureOverride; - boolean prevRendering3D = RenderState.rendering3D; - RenderSystem.outputColorTextureOverride = handMask.getColorTextureView(); - RenderSystem.outputDepthTextureOverride = handMask.getDepthTextureView(); - RenderState.rendering3D = true; - try { - executePreparedHandFrame(prepared); - } finally { - RenderState.rendering3D = prevRendering3D; - RenderSystem.outputColorTextureOverride = prevColor; - RenderSystem.outputDepthTextureOverride = prevDepth; - } - - maskReady = true; - executePreparedHandFrame(prepared); - } - - return true; - } - - private static void executePreparedHandFrame(FeatureRenderDispatcher.PreparedFrame prepared) { - prepared.executeSolid(); - prepared.executeTranslucent(); - prepared.executeTranslucentAfterTerrain(); - prepared.executeAlwaysOnTop(); - } - - public boolean renderIrisHandMask(GameRenderer renderer, - CameraRenderState cameraRenderState, - Matrix4fc positionMatrix, - float tickDelta) { - Minecraft mc = Minecraft.getInstance(); - if (!IrisRuntime.isShaderpackRendererActive() || !isEnabled() || mc.player == null || mc.level == null) { - return false; - } - if (!hands.get() || !shouldRenderHand() || !mc.options.getCameraType().isFirstPerson()) { - return false; - } - if (renderer == null || cameraRenderState == null || positionMatrix == null) { - return false; - } - - FeatureRenderDispatcher dispatcher = getStandaloneHandFeatureDispatcher(); - if (dispatcher == null) { - return false; - } - - maskReady = false; - ensureBuffers(); - if (handMask == null || handMask.getColorTexture() == null) { - return false; - } - - var encoder = RenderSystem.getDevice().createCommandEncoder(); - encoder.clearColorTexture(handMask.getColorTexture(), new org.joml.Vector4f(0.0f, 0.0f, 0.0f, 0.0f)); - if (handMask.getDepthTexture() != null) { - encoder.clearDepthTexture(handMask.getDepthTexture(), 0.0); - } - - GpuTextureView prevColor = RenderSystem.outputColorTextureOverride; - GpuTextureView prevDepth = RenderSystem.outputDepthTextureOverride; - boolean prevRendering3D = RenderState.rendering3D; - RenderSystem.outputColorTextureOverride = handMask.getColorTextureView(); - RenderSystem.outputDepthTextureOverride = handMask.getDepthTextureView(); - RenderState.rendering3D = true; - RenderSystem.backupProjectionMatrix(); - try { - renderIrisHandMaskPhase(renderer, cameraRenderState, positionMatrix, dispatcher, tickDelta, true); - renderIrisHandMaskPhase(renderer, cameraRenderState, positionMatrix, dispatcher, tickDelta, false); - } finally { - ((IrisHandRendererAccessor) HandRenderer.INSTANCE).lovisual$setRenderingSolid(false); - RenderSystem.restoreProjectionMatrix(); - RenderState.rendering3D = prevRendering3D; - RenderSystem.outputColorTextureOverride = prevColor; - RenderSystem.outputDepthTextureOverride = prevDepth; - } - - maskReady = true; - return true; - } - - private void renderIrisHandMaskPhase(GameRenderer renderer, - CameraRenderState cameraRenderState, - Matrix4fc positionMatrix, - FeatureRenderDispatcher dispatcher, - float tickDelta, - boolean solidPhase) { - Minecraft mc = Minecraft.getInstance(); - IrisHandRendererAccessor irisHand = (IrisHandRendererAccessor) HandRenderer.INSTANCE; - irisHand.lovisual$setRenderingSolid(solidPhase); - - PoseStack handMatrices = irisHand.lovisual$invokeSetupGlState(renderer, cameraRenderState, positionMatrix, tickDelta); - handMatrices.pushPose(); - var modelView = RenderSystem.getModelViewStack(); - modelView.pushMatrix(); - modelView.set(handMatrices.last().pose()); - - SubmitNodeStorage handSubmits = new SubmitNodeStorage(); - IrisHandMaskState.renderBothPhases(() -> - ((ItemInHandInterface) renderer.itemInHandRenderer).iris$renderHandsWithCustomRenderer( - HandRenderer.INSTANCE, - tickDelta, - new PoseStack(), - handSubmits, - mc.player, - mc.getEntityRenderDispatcher().getPackedLightCoords(mc.player, tickDelta) - ) - ); - try { - dispatcher.renderAllFeatures(handSubmits); - } finally { - modelView.popMatrix(); - handMatrices.popPose(); - endStandaloneHandFrame(); - } - } - - private boolean shouldRenderHand() { - Freecam fc = Modules.get(Freecam.class); - return fc == null || !fc.isEnabled() || fc.renderHand(); - } - - private void ensureBuffers() { - Minecraft mc = Minecraft.getInstance(); - int w = mc.getWindow().getWidth(); - int h = mc.getWindow().getHeight(); - if (w <= 0 || h <= 0) return; - - if (handMask == null) { - handMask = new TextureTarget("lovisual-hand-mask", w, h, true, GpuFormat.RGBA8_UNORM); - bufferW = w; - bufferH = h; - } else if (w != bufferW || h != bufferH) { - handMask.resize(w, h); - bufferW = w; - bufferH = h; - } - } - - private GpuSampler getHandMaskSampler() { - if (handMaskSampler == null) { - handMaskSampler = RenderSystem.getSamplerCache().getSampler( - AddressMode.CLAMP_TO_EDGE, - AddressMode.CLAMP_TO_EDGE, - FilterMode.NEAREST, - FilterMode.NEAREST, - false - ); - } - return handMaskSampler; - } - - private boolean isSmokeMode() { - return "Smoke".equals(mode.get()); - } - - private boolean isMetallicMode() { - return "Metallic".equals(mode.get()); - } - - private boolean isNoiseMode() { - return "Noise".equals(mode.get()); - } - - private boolean isHandEffectMode() { - return isSmokeMode() || isMetallicMode() || isNoiseMode(); - } - - private boolean isGlowVisible() { - return isHandEffectMode() && glow.get(); - } - - private boolean isShadowVisible() { - return isHandEffectMode() && shadow.get(); - } - - private boolean isCustomEffectColor() { - return isHandEffectMode() && "Custom".equals(effectColorSource.get()); - } - - private boolean isCustomMetallicVisualColor() { - return isMetallicMode() && "Custom".equals(visualColorSource.get()); - } - - public boolean isGlassMode() { - return "Glass".equals(mode.get()); - } - - private boolean isSnowMode() { - return "Snow".equals(mode.get()); - } - - private boolean isChromaMode() { - return "Chroma".equals(mode.get()); - } - - private boolean isSolidMode() { - return "Solid".equals(mode.get()); - } - - private boolean isStripesMode() { - return "Stripes".equals(mode.get()); - } - - private boolean isGlowShaderMode() { - return "Glow".equals(mode.get()); - } - - private boolean isBalatroMode() { - return "Balatro".equals(mode.get()); - } - - private void ensureDualBuffers() { - Minecraft mc = Minecraft.getInstance(); - int w = mc.getWindow().getWidth(); - int h = mc.getWindow().getHeight(); - if (w <= 0 || h <= 0) return; - if (dualA == null) { - dualA = new TextureTarget("lovisual-hand-dual-a", w, h, true, GpuFormat.RGBA8_UNORM); - dualB = new TextureTarget("lovisual-hand-dual-b", w, h, true, GpuFormat.RGBA8_UNORM); - } else if (w != bufferW || h != bufferH) { - dualA.resize(w, h); - dualB.resize(w, h); - } - } - - private int materialFillRgb() { - int fill = fillColor.getArgb(); - if ("Theme".equals(visualColorSource.get())) { - Themes.Theme theme = Theme.theme(); - return forceOpaque(mixRgb(theme.accent(), theme.textPrimary(), 0.18f)); - } - return forceOpaque(fill); - } - - private int metallicBaseRgb() { - if ("Theme".equals(visualColorSource.get())) { - Themes.Theme theme = Theme.theme(); - return forceOpaque(mixRgb(theme.surfaceHover(), theme.accent(), 0.42f)); - } - return forceOpaque(metallicBase.getArgb()); - } - - private int metallicHighlightRgb() { - if ("Theme".equals(visualColorSource.get())) { - Themes.Theme theme = Theme.theme(); - return forceOpaque(mixRgb(theme.textPrimary(), theme.accentSoft(), 0.28f)); - } - return forceOpaque(metallicHighlight.getArgb()); - } - - private int glowRgb(int materialRgb) { - String source = effectColorSource.get(); - if ("Custom".equals(source)) return forceOpaque(glowColor.getArgb()); - if ("Theme".equals(source)) return forceOpaque(Theme.theme().accentSoft()); - return forceOpaque(mixRgb(materialRgb, 0xFFFFFFFF, 0.25f)); - } - - private int shadowRgb(int materialRgb) { - String source = effectColorSource.get(); - if ("Custom".equals(source)) return forceOpaque(shadowColor.getArgb()); - if ("Theme".equals(source)) - return forceOpaque(mixRgb(Theme.theme().windowBg(), Theme.theme().accent(), 0.08f)); - return forceOpaque(mixRgb(materialRgb, 0xFF05070C, 0.72f)); - } - - private boolean renderHands(GpuTextureView src, GpuTextureView dst, float tickDelta) { - Minecraft mc = Minecraft.getInstance(); - if (!maskReady) return false; - if (handMask == null) { - maskReady = false; - return false; - } - - var maskView = handMask.getColorTextureView(); - if (maskView == null) { - maskReady = false; - return false; - } - - float rawTime = getRawTime(); - - if (dualShader.get()) { - renderDualShader(src, dst, maskView, rawTime); - maskReady = false; - return true; - } - - if ("Glass".equals(mode.get())) { - int fillArgb = fillColor.getArgb(); - - float fillR = channel(fillArgb, 16); - float fillG = channel(fillArgb, 8); - float fillB = channel(fillArgb, 0); - float fillA = channel(fillArgb, 24); - - int w = mc.getWindow().getWidth(); - int h = mc.getWindow().getHeight(); - float glassAlpha = hideHand.get() ? 1.0f : clamp01(fillA); - float glassStrength = Math.max(0.0f, fillA * 4.0f); - float glassLevel = Math.min(glassStrength, 4.0f); - - // Hand glass is intentionally blur-first, like Renderer2D liquid glass. - // The item is already rendered in src; this pass only adds frosted refraction on top. - float edgeWidth = 12.0f + glassLevel * 2.25f; - float refractionPx = 5.5f + glassLevel * 2.35f; - float hazeStrength = 0.72f + Math.min(glassStrength, 3.0f) * 0.085f; - float frostBlurPx = 5.0f + glassLevel * 1.65f; - float bodyFrost = 0.48f + Math.min(glassStrength, 3.0f) * 0.105f; - float chromaticPx = 1.65f + glassLevel * 0.42f; - float edgeRefractionMul = 1.15f + glassLevel * 0.11f; - float clarity = 0.08f; - - HandGlassUniforms.update( - w, h, rawTime, edgeWidth, - fillR, fillG, fillB, glassAlpha, - glassStrength, refractionPx, hazeStrength, frostBlurPx, - bodyFrost, chromaticPx, edgeRefractionMul, clarity - ); - - FullScreenRenderer.begin("LoVisual Fullscreen Pass") - .attachment(dst) - .pipeline(LoVisualRenderPipelines.HAND_GLASS) - .uniform("HandGlass", HandGlassUniforms.get()) - .sampler("u_Src", src, PostProcessManager.getSampler()) - .sampler("u_Mask", maskView, getHandMaskSampler()) - .end(); - } else if ("Metallic".equals(mode.get())) { - int fillArgb = fillColor.getArgb(); - int baseRgb = metallicBaseRgb(); - int hiRgb = metallicHighlightRgb(); - int glowRgb = glowRgb(baseRgb); - int shadowRgb = shadowRgb(baseRgb); - - float fillA = hideHand.get() ? 1.0f : channel(fillArgb, 24); - float glowA = glow.get() ? glowAlpha.get() : 0.0f; - float shadowA = shadow.get() ? shadowAlpha.get() : 0.0f; - - HandMetallicUniforms.update( - channel(baseRgb, 16), channel(baseRgb, 8), channel(baseRgb, 0), fillA, - channel(hiRgb, 16), channel(hiRgb, 8), channel(hiRgb, 0), 1.0f, - channel(glowRgb, 16), channel(glowRgb, 8), channel(glowRgb, 0), glowA, - channel(shadowRgb, 16), channel(shadowRgb, 8), channel(shadowRgb, 0), shadowA, - metallicIntensity.get(), metallicSharpness.get(), metallicEdge.get(), rawTime, - metallicSweepSpeed.get(), metallicSweepScale.get(), metallicBrushedLines.get(), metallicFlakes.get(), - glowStrength.get(), shadowStrength.get(), edgeWidth.get(), metallicPrism.get() - ); - - FullScreenRenderer.begin("LoVisual Fullscreen Pass") - .attachment(dst) - .pipeline(LoVisualRenderPipelines.HAND_METALLIC) - .uniform("HandMetallic", HandMetallicUniforms.get()) - .sampler("u_Src", src, PostProcessManager.getSampler()) - .sampler("u_Mask", maskView, getHandMaskSampler()) - .end(); - } else if ("Smoke".equals(mode.get())) { - int fillArgb = fillColor.getArgb(); - int fillRgb = materialFillRgb(); - int glowRgb = glowRgb(fillRgb); - int shadowRgb = shadowRgb(fillRgb); - - float fillA = hideHand.get() ? 1.0f : channel(fillArgb, 24); - float glowA = glow.get() ? glowAlpha.get() : 0.0f; - float shadowA = shadow.get() ? shadowAlpha.get() : 0.0f; - - int w = mc.getWindow().getWidth(); - int h = mc.getWindow().getHeight(); - - HandSmokeUniforms.update( - channel(fillRgb, 16), channel(fillRgb, 8), channel(fillRgb, 0), fillA, - channel(glowRgb, 16), channel(glowRgb, 8), channel(glowRgb, 0), glowA, - channel(shadowRgb, 16), channel(shadowRgb, 8), channel(shadowRgb, 0), shadowA, - edgeWidth.get(), quality.get(), smokeOctaves.get(), rawTime * smokeSpeed.get(), - w, h, smokeScale.get(), smokeContrast.get(), - smokeSwirl.get(), glowStrength.get(), shadowStrength.get(), smokeDensity.get() - ); - - FullScreenRenderer.begin("LoVisual Fullscreen Pass") - .attachment(dst) - .pipeline(LoVisualRenderPipelines.HAND_SMOKE) - .uniform("HandSmoke", HandSmokeUniforms.get()) - .sampler("u_Src", src, PostProcessManager.getSampler()) - .sampler("u_Mask", maskView, getHandMaskSampler()) - .end(); - } else if ("Noise".equals(mode.get())) { - renderNoise(src, dst, maskView, rawTime); - } else { - renderChamsEffect(src, dst, maskView, mode.get(), rawTime); - } - - maskReady = false; - return true; - } - - private void renderChamsEffect(GpuTextureView src, GpuTextureView dst, GpuTextureView maskView, String chamsMode, float rawTime) { - Minecraft mc = Minecraft.getInstance(); - int w = mc.getWindow().getWidth(); - int h = mc.getWindow().getHeight(); - float time = rawTime; - int colorA = 0xFFFFFFFF, colorB = 0xFFFFFFFF, colorC = 0xFFFFFFFF, colorD = 0xFFFFFFFF; - float chromaSpeedV = 0.3f, chromaSat = 0.5f, chromaBright = 1.0f; - float snowInt = 1.0f, snowSpd = 1.0f; - float stripeW = 0.3f, stripeSpd = 0.3f; - boolean glowFill = true; - float glowRadiusV = 0.04f, glowPowerV = 0.75f, glowDisp = 5.0f; - - switch (chamsMode) { - case "Snow" -> { - colorA = snowColor.getArgb(); - snowInt = snowIntensity.get(); - snowSpd = snowSpeed.get(); - } - case "Chroma" -> { - chromaSpeedV = chromaSpeed.get(); - chromaSat = chromaSaturation.get(); - chromaBright = chromaBrightness.get(); - } - case "Solid" -> { - colorA = solidColor1.getArgb(); - colorB = solidColor2.getArgb(); - time = rawTime * solidSpeed.get(); - } - case "Stripes" -> { - colorA = stripesColor1.getArgb(); - colorB = stripesColor2.getArgb(); - stripeW = stripesWidth.get(); - stripeSpd = stripesSpeed.get(); - } - case "Glow" -> { - colorC = glowShaderColor.getArgb(); - glowFill = glowShaderFill.get(); - glowRadiusV = glowShaderRadius.get(); - glowPowerV = glowShaderPower.get(); - glowDisp = glowShaderDispersion.get(); - } - case "Invert" -> { - // шейдер сам ничего не читает из colorA..D — эффект чисто по u_Mix.x/u_Flags.x - } - default -> { - colorA = balatroColor1.getArgb(); - colorB = balatroColor2.getArgb(); - colorC = balatroColor3.getArgb(); - colorD = rgb01(balatroContrast.get(), balatroLighting.get()); - } - } - - HandChamsUniforms.update(colorA, colorB, colorC, colorD, w, h, time, - hideHand.get() ? 1.0f : 0.85f, chromaSpeedV, chromaSat, chromaBright, snowInt, snowSpd, stripeW, stripeSpd, - glowFill, glowRadiusV, glowPowerV, glowDisp, hideHand.get()); - - RenderPipeline pipeline = switch (chamsMode) { - case "Snow" -> LoVisualRenderPipelines.HAND_CHAMS_SNOW; - case "Chroma" -> LoVisualRenderPipelines.HAND_CHAMS_CHROMA; - case "Solid" -> LoVisualRenderPipelines.HAND_CHAMS_SOLID; - case "Stripes" -> LoVisualRenderPipelines.HAND_CHAMS_STRIPES; - case "Glow" -> LoVisualRenderPipelines.HAND_CHAMS_GLOW; - case "Invert" -> LoVisualRenderPipelines.HAND_CHAMS_INVERT; - default -> LoVisualRenderPipelines.HAND_CHAMS_BALATRO; - }; - - FullScreenRenderer.begin("LoVisual Fullscreen Pass") - .attachment(dst) - .pipeline(pipeline) - .uniform("HandChams", HandChamsUniforms.get()) - .sampler("u_Src", src, PostProcessManager.getSampler()) - .sampler("u_Mask", maskView, getHandMaskSampler()) - .end(); - } - - private void renderNoise(GpuTextureView src, GpuTextureView dst, GpuTextureView maskView, float rawTime) { - Minecraft mc = Minecraft.getInstance(); - int fillRgb = materialFillRgb(); - int glowRgb = glowRgb(fillRgb); - - float fillA = hideHand.get() ? 1.0f : clamp01(noiseAlpha.get()); - float glowA = glow.get() ? glowAlpha.get() : 0.0f; - - int w = mc.getWindow().getWidth(); - int h = mc.getWindow().getHeight(); - - HandNoiseUniforms.update( - channel(fillRgb, 16), channel(fillRgb, 8), channel(fillRgb, 0), fillA, - channel(glowRgb, 16), channel(glowRgb, 8), channel(glowRgb, 0), glowA, - edgeWidth.get(), quality.get(), NOISE_OCTAVES, rawTime, - w, h, NOISE_SCALE, NOISE_SPEED, - NOISE_CONTRAST, NOISE_BREATHE, glowStrength.get(), NOISE_DENSITY - ); - - FullScreenRenderer.begin("LoVisual Fullscreen Pass") - .attachment(dst) - .pipeline(LoVisualRenderPipelines.HAND_NOISE) - .uniform("HandNoise", HandNoiseUniforms.get()) - .sampler("u_Src", src, PostProcessManager.getSampler()) - .sampler("u_Mask", maskView, getHandMaskSampler()) - .end(); - } - - private void renderDualShader(GpuTextureView src, GpuTextureView dst, GpuTextureView maskView, float rawTime) { - ensureDualBuffers(); - if (dualA == null || dualB == null - || dualA.getColorTextureView() == null || dualB.getColorTextureView() == null) { - return; - } - clearTexture(dualA); - clearTexture(dualB); - - renderChamsEffect(src, dualA.getColorTextureView(), maskView, shader1.get(), rawTime); - renderChamsEffect(src, dualB.getColorTextureView(), maskView, shader2.get(), rawTime); - - HandBlendUniforms.update(blendModeIndex(), blendIntensity.get(), shader1Strength.get(), shader2Strength.get()); - FullScreenRenderer.begin("LoVisual Fullscreen Pass") - .attachment(dst) - .pipeline(LoVisualRenderPipelines.HAND_CHAMS_BLEND) - .uniform("HandBlend", HandBlendUniforms.get()) - .sampler("u_Shader1", dualA.getColorTextureView(), PostProcessManager.getSampler()) - .sampler("u_Shader2", dualB.getColorTextureView(), PostProcessManager.getSampler()) - .sampler("u_Mask", maskView, getHandMaskSampler()) - .end(); - } - - private int blendModeIndex() { - return switch (blendMode.get()) { - case "Add" -> 1; - case "Multiply" -> 2; - case "Screen" -> 3; - case "Overlay" -> 4; - case "Difference" -> 5; - default -> 0; - }; - } - - private static int rgb01(float r01, float g01) { - int r = Math.round(clamp01(r01) * 255f); - int g = Math.round(clamp01(g01) * 255f); - return 0xFF000000 | (r << 16) | (g << 8); - } - - private static void clearTexture(TextureTarget target) { - if (target == null || target.getColorTexture() == null) return; - var encoder = RenderSystem.getDevice().createCommandEncoder(); - encoder.clearColorTexture(target.getColorTexture(), new org.joml.Vector4f(0.0f, 0.0f, 0.0f, 0.0f)); - } - - private float getRawTime() { - return (float) (Util.getMillis() / 1000.0); - } - - private final class HandsPass implements PostProcessPass { - @Override - public boolean isActive() { - Minecraft mc = Minecraft.getInstance(); - return Chams.this.isEnabled() - && mc.player != null - && mc.level != null - && hands.get() - && mc.options.getCameraType().isFirstPerson(); - } - - @Override - public Phase getPhase() { - return Phase.POST_HAND; - } - - @Override - public boolean render(GpuTextureView src, GpuTextureView dst, float tickDelta) { - Minecraft mc = Minecraft.getInstance(); - if (!Chams.this.isEnabled() || mc.player == null || mc.level == null) return false; - if (!hands.get() || !mc.options.getCameraType().isFirstPerson()) return false; - return Chams.this.renderHands(src, dst, tickDelta); - } - } - -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/precip/Rain.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/precip/Rain.java deleted file mode 100644 index c06b2e2..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/precip/Rain.java +++ /dev/null @@ -1,903 +0,0 @@ -package dev.loki.lovisual.features.module.modules.visuals.weather.precip; - -import dev.loki.lovisual.config.values.primitive.BooleanValue; -import dev.loki.lovisual.config.values.mode.EnumValue; -import dev.loki.lovisual.config.values.primitive.NumberValue; -import dev.loki.lovisual.config.values.color.RGBAColorValue; -import dev.loki.lovisual.features.module.core.Module; -import dev.loki.lovisual.features.module.core.ModuleCategory; -import dev.loki.lovisual.features.module.core.ModuleInfo; -import dev.loki.lovisual.features.module.phase.WorldPhase; -import dev.loki.lovisual.features.theme.Theme; -import dev.loki.lovisual.render.engine.RenderState; -import dev.loki.lovisual.render.engine.core.system.LoVisualWorldMatrices; -import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; -import dev.loki.lovisual.render.engine.postprocess.post.PostProcessContext; -import dev.loki.lovisual.render.engine.postprocess.post.PostProcessManager; -import dev.loki.lovisual.render.engine.postprocess.post.PostProcessPass; -import dev.loki.lovisual.render.engine.renderer.FullScreenRenderer; -import dev.loki.lovisual.render.engine.renderer.Renderer3D; -import dev.loki.lovisual.render.engine.uniform.MeshBuilder; -import dev.loki.lovisual.render.engine.uniform.impl.world.liquid.WetSurfaceUniforms; -import com.mojang.blaze3d.textures.GpuTextureView; -import net.minecraft.client.Camera; -import net.minecraft.client.Minecraft; -import net.minecraft.core.BlockPos; -import net.minecraft.util.Mth; -import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.level.levelgen.Heightmap; -import net.minecraft.world.phys.Vec3; -import org.joml.Matrix4f; -import org.joml.Quaternionf; -import org.joml.Vector3f; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Random; - -/** - * Volumetric rain: falling drop streaks, ground splashes with debris droplets, ground mist and lightning flashes. - * Wet-ground screen-space reflections are a separate follow-up (see {@link #renderWetSurfaceFrame}). - */ -@ModuleInfo( - id = "rain", - displayName = "Rain", - category = ModuleCategory.VISUALS, - description = "Реалистичный объёмный дождь с брызгами, туманом и молниями" -) -public final class Rain extends Module implements PostProcessPass { - - private final Matrix4f wetSurfaceProjection = new Matrix4f(); - private final Matrix4f wetSurfaceInvView = new Matrix4f(); - private boolean wetSurfaceDepthSupported = true; - - { - PostProcessManager.register(this); - } - - private static final double GRAVITY = 22.0; - private static final double TERMINAL_VELOCITY = 34.0; - private static final int RIPPLE_SEGMENTS = 14; - private static final int MAX_DROPS = 4200; - private static final int MAX_MIST = 90; - - private final EnumValue preset = enumMode("preset", Preset.RAIN, Preset.values()); - private final NumberValue density = num("density", 1.0f, 0.1f, 3.0f); - private final NumberValue radius = num("radius", 26.0f, 8.0f, 64.0f); - private final NumberValue altitude = num("altitude", 16.0f, 6.0f, 40.0f); - private final NumberValue dropSize = num("drop_size", 1.0f, 0.3f, 2.5f); - private final NumberValue fallSpeed = num("fall_speed", 1.0f, 0.3f, 2.5f); - private final NumberValue wind = num("wind", 1.0f, 0.0f, 3.0f); - private final NumberValue opacity = num("opacity", 1.0f, 0.1f, 2.0f); - - private final BooleanValue splashes = bool("splashes", true); - private final BooleanValue droplets = visibleWhen(bool("splash_droplets", true), splashes::get); - private final BooleanValue groundMist = bool("ground_mist", true); - private final BooleanValue lightning = bool("lightning", false); - private final BooleanValue skyCheck = bool("sky_check", true); - private final BooleanValue onlyWhenRaining = bool("only_when_raining", false); - - private final EnumValue colorMode = enumMode("color_mode", ColorMode.REALISTIC, ColorMode.values()); - private final RGBAColorValue customColor = visibleWhen(color("color", "#FFBFD8E6"), () -> colorMode.get() == ColorMode.CUSTOM); - - private final BooleanValue wetGround = bool("wet_ground", true); - private final NumberValue wetness = visibleWhen(num("wetness", 1.25f, 0.1f, 2.0f), wetGround::get); - private final NumberValue puddleCoverage = visibleWhen(num("puddle_coverage", 0.90f, 0.05f, 1.0f), wetGround::get); - private final NumberValue reflectionStrength = visibleWhen(num("reflection_strength", 1.55f, 0.1f, 2.0f), wetGround::get); - private final NumberValue reflectionDistance = visibleWhen(num("reflection_distance", 56.0f, 12.0f, 96.0f), wetGround::get); - private final EnumValue reflectionQuality = visibleWhen(enumMode("reflection_quality", ReflectionQuality.BALANCED, ReflectionQuality.values()), wetGround::get); - private final BooleanValue puddleRipples = visibleWhen(bool("puddle_ripples", true), wetGround::get); - private final NumberValue rippleStrength = visibleWhen(num("ripple_strength", 0.8f, 0.0f, 1.6f), () -> wetGround.get() && puddleRipples.get()); - - private final List drops = new ArrayList<>(); - private final List splashList = new ArrayList<>(); - private final List dropletList = new ArrayList<>(); - private final List mistList = new ArrayList<>(); - private final Map groundCache = new HashMap<>(); - private final BlockPos.MutableBlockPos scratchPos = new BlockPos.MutableBlockPos(); - private final Random random = new Random(); - - private long lastFrameNanos; - private float clock; - private float flash; - private float nextFlashIn = 9.0f; - private int cacheTicks; - - @Override - public void onEnable() { - lastFrameNanos = 0L; - wetSurfaceDepthSupported = true; - clearAll(); - } - - @Override - public void onDisable() { - clearAll(); - } - - private void clearAll() { - drops.clear(); - splashList.clear(); - dropletList.clear(); - mistList.clear(); - groundCache.clear(); - flash = 0.0f; - } - - @Override - public void onTick() { - Minecraft mc = Minecraft.getInstance(); - if (mc.player == null || mc.level == null) { - clearAll(); - return; - } - if (++cacheTicks > 30) { - cacheTicks = 0; - groundCache.clear(); - } - } - - private float presetDensity() { - return switch (preset.get()) { - case DRIZZLE -> 0.45f; - case DOWNPOUR -> 1.9f; - case STORM -> 2.8f; - case RAIN -> 1.0f; - }; - } - - private float presetWind() { - return switch (preset.get()) { - case DRIZZLE -> 0.5f; - case DOWNPOUR -> 1.35f; - case STORM -> 2.4f; - case RAIN -> 1.0f; - }; - } - - private float presetMass() { - return switch (preset.get()) { - case DRIZZLE -> 0.55f; - case DOWNPOUR -> 1.25f; - case STORM -> 1.5f; - case RAIN -> 1.0f; - }; - } - - private int targetDropCount() { - float r = radius.get(); - float area = r * r * 0.0155f; - int count = (int) (area * density.get() * presetDensity() * 26.0f); - return Mth.clamp(count, 40, MAX_DROPS); - } - - private int resolveColorArgb() { - return switch (colorMode.get()) { - case CUSTOM -> customColor.getArgb(); - case THEME -> Theme.theme().accent(); - case REALISTIC -> 0xFFC8DCE8; - }; - } - - private float random(float min, float max) { - return min + random.nextFloat() * (max - min); - } - - private int surfaceY(int x, int z) { - Minecraft mc = Minecraft.getInstance(); - long key = (((long) x) << 32) ^ (z & 0xFFFFFFFFL); - Integer cached = groundCache.get(key); - if (cached != null) return cached; - - int top; - try { - top = mc.level.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING, new BlockPos(x, 0, z)).getY(); - } catch (Throwable ignored) { - top = mc.level.getMinY(); - } - if (groundCache.size() < 20000) groundCache.put(key, top); - return top; - } - - private boolean isBlocking(double x, double y, double z) { - Minecraft mc = Minecraft.getInstance(); - if (mc.level == null) return false; - scratchPos.set(Mth.floor(x), Mth.floor(y), Mth.floor(z)); - - BlockState state = mc.level.getBlockState(scratchPos); - if (!state.getFluidState().isEmpty()) return true; - if (state.isAir()) return false; - try { - return !state.getCollisionShape(mc.level, scratchPos).isEmpty(); - } catch (Throwable ignored) { - return true; - } - } - - private boolean isWater(double x, double y, double z) { - Minecraft mc = Minecraft.getInstance(); - if (mc.level == null) return false; - scratchPos.set(Mth.floor(x), Mth.floor(y), Mth.floor(z)); - try { - return !mc.level.getBlockState(scratchPos).getFluidState().isEmpty(); - } catch (Throwable ignored) { - return false; - } - } - - private boolean skyVisible(double x, double y, double z) { - Minecraft mc = Minecraft.getInstance(); - if (!skyCheck.get() || mc.level == null) return true; - scratchPos.set(Mth.floor(x), Mth.floor(y), Mth.floor(z)); - try { - return mc.level.canSeeSky(scratchPos); - } catch (Throwable ignored) { - return true; - } - } - - private void spawnDrop(Vec3 center, boolean fromTop) { - float r = radius.get(); - double angle = random.nextDouble() * Math.PI * 2.0; - double dist = Math.sqrt(random.nextDouble()) * r; - - double x = center.x + Math.cos(angle) * dist; - double z = center.z + Math.sin(angle) * dist; - double y = fromTop - ? center.y + altitude.get() * random(0.75f, 1.0f) - : center.y + random(-4.0f, altitude.get()); - - if (!skyVisible(x, y, z)) return; - - Drop drop = new Drop(); - drop.x = x; - drop.y = y; - drop.z = z; - drop.mass = random(0.6f, 1.4f) * presetMass(); - drop.vy = -random(6.0f, 11.0f) * drop.mass; - drop.phase = random(0.0f, 62.8f); - drop.brightness = random(0.55f, 1.0f); - drop.ground = surfaceY(Mth.floor(x), Mth.floor(z)); - drops.add(drop); - } - - private void updateDrops(Vec3 center, float dt) { - int target = targetDropCount(); - - drops.removeIf(d -> { - double dx = d.x - center.x; - double dz = d.z - center.z; - float r = radius.get() + 6.0f; - return d.dead || dx * dx + dz * dz > r * r || d.y < center.y - altitude.get() - 12.0; - }); - - boolean fill = drops.size() < target / 2; - int spawnBudget = fill ? Math.min(target - drops.size(), 900) : Math.min(target - drops.size(), 260); - for (int i = 0; i < spawnBudget; i++) { - spawnDrop(center, !fill); - } - - float speedMul = fallSpeed.get(); - float windMul = wind.get() * presetWind(); - - for (int i = 0, size = drops.size(); i < size; i++) { - Drop d = drops.get(i); - - double gust = 0.65 + 0.35 * Math.sin(clock * 0.55 + d.phase * 0.15) - + 0.18 * Math.sin(clock * 1.7 + d.phase); - double windX = windMul * 3.1 * gust; - double windZ = windMul * 1.7 * Math.sin(clock * 0.31 + d.phase * 0.05) * gust; - - double drag = 1.0 - Math.min(0.92, Math.abs(d.vy) / (TERMINAL_VELOCITY * d.mass)); - d.vy -= GRAVITY * d.mass * drag * dt; - if (d.vy < -TERMINAL_VELOCITY * d.mass) d.vy = -TERMINAL_VELOCITY * d.mass; - - d.vx += (windX - d.vx) * Math.min(1.0, dt * 3.2); - d.vz += (windZ - d.vz) * Math.min(1.0, dt * 3.2); - - double nx = d.x + d.vx * dt * speedMul; - double ny = d.y + d.vy * dt * speedMul; - double nz = d.z + d.vz * dt * speedMul; - - boolean hit = false; - if (ny <= d.ground + 2.5) { - if (isBlocking(nx, ny, nz)) { - hit = true; - } else if (ny <= d.ground - 3.0) { - d.dead = true; - } - } - - if (hit) { - if (splashes.get()) addSplash(nx, ny, nz, d); - d.dead = true; - continue; - } - - d.x = nx; - d.y = ny; - d.z = nz; - } - } - - private void addSplash(double x, double y, double z, Drop d) { - if (splashList.size() > 700) return; - - double impactY = Math.floor(y) + 1.0005; - Splash splash = new Splash(); - splash.x = x; - splash.y = impactY; - splash.z = z; - splash.life = 0.0f; - splash.maxLife = random(0.30f, 0.52f); - splash.scale = (float) (0.32 + 0.22 * d.mass); - splash.water = isWater(x, y - 0.15, z); - splashList.add(splash); - - if (!droplets.get() || dropletList.size() > 1400) return; - - int shards = splash.water ? 4 : 3; - for (int i = 0; i < shards; i++) { - double a = random.nextDouble() * Math.PI * 2.0; - double sp = random(1.1f, 2.6f) * d.mass; - Droplet drop = new Droplet(); - drop.x = x; - drop.y = impactY + 0.02; - drop.z = z; - drop.vx = Math.cos(a) * sp * 0.45 + d.vx * 0.08; - drop.vz = Math.sin(a) * sp * 0.45 + d.vz * 0.08; - drop.vy = random(1.9f, 4.1f); - drop.maxLife = random(0.28f, 0.5f); - dropletList.add(drop); - } - } - - private void updateDroplets(float dt) { - for (int i = dropletList.size() - 1; i >= 0; i--) { - Droplet dr = dropletList.get(i); - dr.life += dt; - if (dr.life >= dr.maxLife) { - dropletList.remove(i); - continue; - } - dr.vy -= GRAVITY * 0.55 * dt; - dr.x += dr.vx * dt; - dr.y += dr.vy * dt; - dr.z += dr.vz * dt; - } - } - - private void updateSplashes(float dt) { - for (int i = splashList.size() - 1; i >= 0; i--) { - Splash s = splashList.get(i); - s.life += dt; - if (s.life >= s.maxLife) splashList.remove(i); - } - } - - private void updateMist(Vec3 center, float dt) { - if (!groundMist.get()) { - mistList.clear(); - return; - } - - int target = (int) Mth.clamp(22 * density.get() * presetDensity(), 6, MAX_MIST); - - mistList.removeIf(m -> { - m.life += dt; - double dx = m.x - center.x; - double dz = m.z - center.z; - float r = radius.get(); - m.x += m.vx * dt; - m.z += m.vz * dt; - return m.life > m.maxLife || dx * dx + dz * dz > r * r; - }); - - while (mistList.size() < target) { - double angle = random.nextDouble() * Math.PI * 2.0; - double dist = Math.sqrt(random.nextDouble()) * radius.get() * 0.85; - double x = center.x + Math.cos(angle) * dist; - double z = center.z + Math.sin(angle) * dist; - int ground = surfaceY(Mth.floor(x), Mth.floor(z)); - - Mist m = new Mist(); - m.x = x; - m.z = z; - m.y = ground + random(0.05f, 0.9f); - m.size = random(2.2f, 5.6f); - m.maxLife = random(2.4f, 5.5f); - m.life = random(0.0f, 1.0f); - m.vx = random(-0.35f, 0.35f) * wind.get(); - m.vz = random(-0.35f, 0.35f) * wind.get(); - mistList.add(m); - } - } - - private void updateLightning(float dt) { - if (!lightning.get()) { - flash = 0.0f; - return; - } - flash = Math.max(0.0f, flash - dt * 3.4f); - nextFlashIn -= dt; - if (nextFlashIn <= 0.0f) { - flash = random(0.55f, 1.0f); - nextFlashIn = random(6.0f, 16.0f); - } - } - - private float ambientLight(Camera camera) { - Minecraft mc = Minecraft.getInstance(); - try { - int light = mc.level.getBrightness(net.minecraft.world.level.LightLayer.BLOCK, camera.blockPosition()); - return Mth.clamp(light / 15.0f, 0.3f, 1.0f); - } catch (Throwable ignored) { - return 1.0f; - } - } - - // --- Wet-ground screen-space reflection pass (PostProcessPass) --- - - private int reflectionSteps() { - return switch (reflectionQuality.get()) { - case HIGH -> 16; - case BALANCED -> 10; - case PERFORMANCE -> 6; - }; - } - - private int binarySteps() { - return switch (reflectionQuality.get()) { - case HIGH -> 6; - case BALANCED -> 4; - case PERFORMANCE -> 3; - }; - } - - private float rainAmount() { - return Mth.clamp(0.55f + density.get() * presetDensity() * 0.28f, 0.55f, 1.45f); - } - - @Override - public boolean isActive() { - Minecraft mc = Minecraft.getInstance(); - return isEnabled() - && wetGround.get() - && wetSurfaceDepthSupported - && mc.player != null - && mc.level != null - && (!onlyWhenRaining.get() || mc.level.isRaining()) - && playerSeesSky(mc); - } - - /** - * Gates the screen-space wet-surface pass by the player's own sky exposure. The shader - * has no per-pixel occlusion, so without this it wets cave floors and undersides of leaf - * canopy whenever it's raining anywhere in the world. Checking the player's own position - * (rather than every visible surface) is an approximation, but it correctly stops the - * effect the moment the player themself is indoors/underground/under leaves — the two - * cases actually being reported. - */ - private boolean playerSeesSky(Minecraft mc) { - if (!skyCheck.get() || mc.player == null || mc.level == null) return true; - try { - return mc.level.canSeeSky(mc.player.blockPosition()); - } catch (Throwable ignored) { - return true; - } - } - - @Override - public int getPriority() { - return 2; - } - - @Override - public Phase getPhase() { - return Phase.PRE_HAND; - } - - @Override - public boolean render(GpuTextureView src, GpuTextureView dst, float tickDelta) { - return false; - } - - @Override - public boolean render(PostProcessContext context, GpuTextureView src, GpuTextureView dst) { - if (!isActive() || context == null || src == null || dst == null) return false; - - GpuTextureView mainDepth = context.mainDepth(); - boolean hasDepth = mainDepth != null; - if (!hasDepth) mainDepth = src; - - Matrix4f projection = LoVisualWorldMatrices.renderProjectionMatrix(); - if (projection == null) projection = RenderState.worldProjection; - wetSurfaceProjection.set(projection); - wetSurfaceInvView.set(new Matrix4f().rotation(RenderState.cameraRotation)); - - WetSurfaceUniforms.update( - wetSurfaceProjection, - wetSurfaceInvView, - (float) RenderState.cameraPos.x, (float) RenderState.cameraPos.y, (float) RenderState.cameraPos.z, - context.width() > 0 ? 1.0f / context.width() : 0.0f, - context.height() > 0 ? 1.0f / context.height() : 0.0f, - clock, - hasDepth, - wetness.get(), - puddleCoverage.get(), - reflectionStrength.get(), - puddleRipples.get() ? rippleStrength.get() : 0.0f, - reflectionDistance.get(), - reflectionSteps(), - rainAmount(), - puddleRipples.get(), - binarySteps(), - 0.18f - ); - - try { - FullScreenRenderer.ensureInit(); - FullScreenRenderer.begin("LoVisual WetSurface Pass") - .attachment(dst) - .pipeline(LoVisualRenderPipelines.WET_SURFACE) - .uniform("WetSurface", WetSurfaceUniforms.get()) - .sampler("u_Texture", src, PostProcessManager.getSampler()) - .sampler("u_MainDepth", mainDepth, PostProcessManager.getSampler()) - .end(); - } catch (Throwable t) { - wetSurfaceDepthSupported = false; - return false; - } - - return true; - } - - @Override - public WorldPhase getWorldPhase() { - return WorldPhase.AFTER_POST_PROCESS; - } - - @Override - public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { - Minecraft mc = Minecraft.getInstance(); - if (!isEnabled() || mc.player == null || mc.level == null || mc.gameRenderer == null) return; - if (mc.gameRenderer.mainCamera() == null) return; - if (onlyWhenRaining.get() && !mc.level.isRaining()) { - clearAll(); - return; - } - - Camera camera = mc.gameRenderer.mainCamera(); - - long now = System.nanoTime(); - if (lastFrameNanos == 0L) lastFrameNanos = now; - float dt = (float) ((now - lastFrameNanos) / 1_000_000_000.0); - lastFrameNanos = now; - dt = Mth.clamp(dt, 0.0f, 0.05f); - clock += dt; - - Vec3 cam = camera.position(); - - updateDrops(cam, dt); - updateSplashes(dt); - updateDroplets(dt); - updateMist(cam, dt); - updateLightning(dt); - - if (drops.isEmpty() && splashList.isEmpty() && dropletList.isEmpty() && mistList.isEmpty()) return; - - int colorArgb = resolveColorArgb(); - float r = ((colorArgb >>> 16) & 0xFF) / 255f; - float g = ((colorArgb >>> 8) & 0xFF) / 255f; - float b = (colorArgb & 0xFF) / 255f; - float[] rgb = {r, g, b}; - - float ambient = ambientLight(camera); - float brightnessMul = Mth.clamp(ambient + flash * 0.9f, 0.0f, 1.65f); - float alphaMul = opacity.get() * brightnessMul; - - Quaternionf camRot = RenderState.cameraRotation; - float camYaw = camera.yRot(); - - MeshBuilder blendMesh = renderer.batch(LoVisualRenderPipelines.WORLD_COLORED, Renderer3D.DepthMode.MAIN); - if (blendMesh != null) { - if (groundMist.get()) drawMist(blendMesh, camRot, rgb, alphaMul); - drawStreaks(blendMesh, camYaw, rgb, alphaMul, false); - if (splashes.get()) drawRipples(blendMesh, rgb, alphaMul); - } - - MeshBuilder additiveMesh = renderer.batch(LoVisualRenderPipelines.WORLD_COLORED_ADDITIVE_DEPTH, Renderer3D.DepthMode.PRE_DEPTH); - if (additiveMesh != null) { - drawStreaks(additiveMesh, camYaw, rgb, alphaMul, true); - if (splashes.get()) { - drawCrowns(additiveMesh, camYaw, rgb, alphaMul); - if (droplets.get()) drawDroplets(additiveMesh, camYaw, rgb, alphaMul); - } - } - } - - private static int packColor(float r, float g, float b, float a) { - int ai = Mth.clamp(Math.round(a * 255.0f), 0, 255); - int ri = Mth.clamp(Math.round(r * 255.0f), 0, 255); - int gi = Mth.clamp(Math.round(g * 255.0f), 0, 255); - int bi = Mth.clamp(Math.round(b * 255.0f), 0, 255); - return (ai << 24) | (ri << 16) | (gi << 8) | bi; - } - - private static Vector3f rotated(Quaternionf rot, float x, float y, float z) { - return new Vector3f(x, y, z).rotate(rot); - } - - private void drawStreaks(MeshBuilder mesh, float camYaw, float[] rgb, float alphaMul, boolean highlight) { - if (drops.isEmpty()) return; - - float sizeMul = dropSize.get(); - float r = radius.get(); - float fadeStart = r * 0.62f; - - for (int i = 0, size = drops.size(); i < size; i++) { - Drop d = drops.get(i); - - double speed = Math.sqrt(d.vx * d.vx + d.vy * d.vy + d.vz * d.vz); - if (speed < 0.0001) continue; - - float length = (float) Mth.clamp((float) (speed * 0.055 * sizeMul), 0.16f, 2.6f); - float width = (float) (0.014 + 0.019 * d.mass) * sizeMul * (highlight ? 0.45f : 1.0f); - - float yawRad = (float) Math.toRadians(camYaw); - double rightX = Math.cos(yawRad); - double rightZ = Math.sin(yawRad); - double horizontal = d.vx * rightX + d.vz * rightZ; - float tilt = (float) Math.toDegrees(Math.atan2(horizontal, Math.abs(d.vy) + 0.0001)); - tilt = Mth.clamp(tilt, -55.0f, 55.0f); - - Quaternionf orient = new Quaternionf() - .rotateY((float) Math.toRadians(-camYaw)) - .rotateZ((float) Math.toRadians(-tilt)); - - float half = width * 0.5f; - float top = length * 0.5f; - float bottom = -length * 0.5f; - - float headAlpha = alphaMul * d.brightness * (highlight ? 0.30f : 0.42f); - if (headAlpha <= 0.006f) continue; - headAlpha = Math.min(headAlpha, 0.95f); - float tailAlpha = 0.0f; - float headWidth = half; - float tailWidth = half * 0.35f; - - Vector3f p1 = rotated(orient, -tailWidth, top, 0.0f); - Vector3f p2 = rotated(orient, tailWidth, top, 0.0f); - Vector3f p3 = rotated(orient, headWidth, bottom, 0.0f); - Vector3f p4 = rotated(orient, -headWidth, bottom, 0.0f); - - mesh.ensureQuadCapacity(); - int i1 = mesh.vec3(d.x + p1.x, d.y + p1.y, d.z + p1.z).color(packColorObj(rgb, tailAlpha)).next(); - int i2 = mesh.vec3(d.x + p2.x, d.y + p2.y, d.z + p2.z).color(packColorObj(rgb, tailAlpha)).next(); - int i3 = mesh.vec3(d.x + p3.x, d.y + p3.y, d.z + p3.z).color(packColorObj(rgb, headAlpha)).next(); - int i4 = mesh.vec3(d.x + p4.x, d.y + p4.y, d.z + p4.z).color(packColorObj(rgb, headAlpha)).next(); - mesh.quad(i1, i2, i3, i4); - } - } - - private void drawRipples(MeshBuilder mesh, float[] rgb, float alphaMul) { - if (splashList.isEmpty()) return; - - for (int i = 0, size = splashList.size(); i < size; i++) { - Splash s = splashList.get(i); - float progress = Mth.clamp(s.life / s.maxLife, 0.0f, 1.0f); - float eased = 1.0f - (1.0f - progress) * (1.0f - progress); - - float outer = s.scale * (0.18f + eased * (s.water ? 1.35f : 0.85f)); - float inner = outer * (0.55f + 0.35f * eased); - float alpha = alphaMul * (1.0f - progress) * (s.water ? 0.5f : 0.38f); - if (alpha <= 0.01f) continue; - - for (int seg = 0; seg < RIPPLE_SEGMENTS; seg++) { - double a0 = (Math.PI * 2.0 / RIPPLE_SEGMENTS) * seg; - double a1 = (Math.PI * 2.0 / RIPPLE_SEGMENTS) * (seg + 1); - - float ix0 = (float) (Math.cos(a0) * inner); - float iz0 = (float) (Math.sin(a0) * inner); - float ix1 = (float) (Math.cos(a1) * inner); - float iz1 = (float) (Math.sin(a1) * inner); - float ox0 = (float) (Math.cos(a0) * outer); - float oz0 = (float) (Math.sin(a0) * outer); - float ox1 = (float) (Math.cos(a1) * outer); - float oz1 = (float) (Math.sin(a1) * outer); - - mesh.ensureQuadCapacity(); - int i1 = mesh.vec3(s.x + ix0, s.y, s.z + iz0).color(packColorObj(rgb, alpha)).next(); - int i2 = mesh.vec3(s.x + ix1, s.y, s.z + iz1).color(packColorObj(rgb, alpha)).next(); - int i3 = mesh.vec3(s.x + ox1, s.y, s.z + oz1).color(packColorObj(rgb, 0.0f)).next(); - int i4 = mesh.vec3(s.x + ox0, s.y, s.z + oz0).color(packColorObj(rgb, 0.0f)).next(); - mesh.quad(i1, i2, i3, i4); - } - } - } - - private void drawCrowns(MeshBuilder mesh, float camYaw, float[] rgb, float alphaMul) { - if (splashList.isEmpty()) return; - - Quaternionf orient = new Quaternionf().rotateY((float) Math.toRadians(-camYaw)); - - for (int i = 0, size = splashList.size(); i < size; i++) { - Splash s = splashList.get(i); - float progress = Mth.clamp(s.life / s.maxLife, 0.0f, 1.0f); - if (progress > 0.55f) continue; - - float local = progress / 0.55f; - float height = s.scale * (0.55f * (float) Math.sin(local * Math.PI)); - float alpha = alphaMul * (1.0f - local) * 0.32f; - if (alpha <= 0.01f || height <= 0.005f) continue; - - float base = s.scale * 0.16f; - Vector3f p1 = rotated(orient, -base * 0.25f, height, 0.0f); - Vector3f p2 = rotated(orient, base * 0.25f, height, 0.0f); - Vector3f p3 = rotated(orient, base, 0.0f, 0.0f); - Vector3f p4 = rotated(orient, -base, 0.0f, 0.0f); - - mesh.ensureQuadCapacity(); - int i1 = mesh.vec3(s.x + p1.x, s.y + p1.y, s.z + p1.z).color(packColorObj(rgb, 0.0f)).next(); - int i2 = mesh.vec3(s.x + p2.x, s.y + p2.y, s.z + p2.z).color(packColorObj(rgb, 0.0f)).next(); - int i3 = mesh.vec3(s.x + p3.x, s.y + p3.y, s.z + p3.z).color(packColorObj(rgb, alpha)).next(); - int i4 = mesh.vec3(s.x + p4.x, s.y + p4.y, s.z + p4.z).color(packColorObj(rgb, alpha)).next(); - mesh.quad(i1, i2, i3, i4); - } - } - - private void drawDroplets(MeshBuilder mesh, float camYaw, float[] rgb, float alphaMul) { - if (dropletList.isEmpty()) return; - - float sizeMul = dropSize.get(); - float yawRad = (float) Math.toRadians(camYaw); - - for (int i = 0, size = dropletList.size(); i < size; i++) { - Droplet d = dropletList.get(i); - float progress = Mth.clamp(d.life / d.maxLife, 0.0f, 1.0f); - float alpha = alphaMul * (1.0f - progress) * 0.5f; - if (alpha <= 0.01f) continue; - - double speed = Math.sqrt(d.vx * d.vx + d.vy * d.vy + d.vz * d.vz); - float length = (float) Mth.clamp((float) (speed * 0.03 * sizeMul), 0.04f, 0.4f); - float width = 0.016f * sizeMul; - - double horizontal = d.vx * Math.cos(yawRad) + d.vz * Math.sin(yawRad); - float tilt = (float) Math.toDegrees(Math.atan2(horizontal, -d.vy + 0.0001)); - - Quaternionf orient = new Quaternionf() - .rotateY((float) Math.toRadians(-camYaw)) - .rotateZ((float) Math.toRadians(-tilt)); - - Vector3f p1 = rotated(orient, -width * 0.4f, length * 0.5f, 0.0f); - Vector3f p2 = rotated(orient, width * 0.4f, length * 0.5f, 0.0f); - Vector3f p3 = rotated(orient, width, -length * 0.5f, 0.0f); - Vector3f p4 = rotated(orient, -width, -length * 0.5f, 0.0f); - - mesh.ensureQuadCapacity(); - int i1 = mesh.vec3(d.x + p1.x, d.y + p1.y, d.z + p1.z).color(packColorObj(rgb, 0.0f)).next(); - int i2 = mesh.vec3(d.x + p2.x, d.y + p2.y, d.z + p2.z).color(packColorObj(rgb, 0.0f)).next(); - int i3 = mesh.vec3(d.x + p3.x, d.y + p3.y, d.z + p3.z).color(packColorObj(rgb, alpha)).next(); - int i4 = mesh.vec3(d.x + p4.x, d.y + p4.y, d.z + p4.z).color(packColorObj(rgb, alpha)).next(); - mesh.quad(i1, i2, i3, i4); - } - } - - private void drawMist(MeshBuilder mesh, Quaternionf camRot, float[] rgb, float alphaMul) { - if (mistList.isEmpty()) return; - - for (int i = 0, size = mistList.size(); i < size; i++) { - Mist mist = mistList.get(i); - float progress = Mth.clamp(mist.life / mist.maxLife, 0.0f, 1.0f); - float fade = (float) Math.sin(progress * Math.PI); - float alpha = alphaMul * fade * 0.05f; - if (alpha <= 0.004f) continue; - - float half = mist.size * 0.5f; - float top = mist.size * 0.42f; - - Vector3f right = new Vector3f(1.0f, 0.0f, 0.0f).rotate(camRot); - Vector3f up = new Vector3f(0.0f, 1.0f, 0.0f).rotate(camRot); - - double bx = mist.x, by = mist.y, bz = mist.z; - - mesh.ensureQuadCapacity(); - int i1 = mesh.vec3(bx - right.x * half, by - right.y * half + up.y * top, bz - right.z * half).color(packColorObj(rgb, 0.0f)).next(); - int i2 = mesh.vec3(bx + right.x * half, by + right.y * half + up.y * top, bz + right.z * half).color(packColorObj(rgb, 0.0f)).next(); - int i3 = mesh.vec3(bx + right.x * half, by + right.y * half, bz + right.z * half).color(packColorObj(rgb, alpha)).next(); - int i4 = mesh.vec3(bx - right.x * half, by - right.y * half, bz - right.z * half).color(packColorObj(rgb, alpha)).next(); - mesh.quad(i1, i2, i3, i4); - } - } - - private dev.loki.lovisual.render.engine.color.RenderColor packColorObj(float[] rgb, float a) { - return new dev.loki.lovisual.render.engine.color.RenderColor(packColor(rgb[0], rgb[1], rgb[2], a)); - } - - public enum Preset implements EnumValue.IdProvider { - DRIZZLE("drizzle"), - RAIN("rain"), - DOWNPOUR("downpour"), - STORM("storm"); - - private final String id; - - Preset(String id) { - this.id = id; - } - - @Override - public String id() { - return id; - } - } - - public enum ColorMode implements EnumValue.IdProvider { - REALISTIC("realistic"), - THEME("theme"), - CUSTOM("custom"); - - private final String id; - - ColorMode(String id) { - this.id = id; - } - - @Override - public String id() { - return id; - } - } - - public enum ReflectionQuality implements EnumValue.IdProvider { - PERFORMANCE("performance"), - BALANCED("balanced"), - HIGH("high"); - - private final String id; - - ReflectionQuality(String id) { - this.id = id; - } - - @Override - public String id() { - return id; - } - } - - private static final class Drop { - double x, y, z; - double vx, vy, vz; - double mass = 1.0; - double ground; - float phase; - float brightness = 1.0f; - boolean dead; - } - - private static final class Droplet { - double x, y, z; - double vx, vy, vz; - float life; - float maxLife = 0.4f; - } - - private static final class Splash { - double x, y, z; - float life; - float maxLife = 0.4f; - float scale = 0.4f; - boolean water; - } - - private static final class Mist { - double x, y, z; - double vx, vz; - float size = 3.0f; - float life; - float maxLife = 4.0f; - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/main/GameRendererMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/main/GameRendererMixin.java deleted file mode 100644 index f38bf5e..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/main/GameRendererMixin.java +++ /dev/null @@ -1,830 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.mixins.render.main; - -import com.llamalad7.mixinextras.injector.wrapoperation.Operation; -import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; -import com.mojang.blaze3d.buffers.GpuBufferSlice; -import com.mojang.blaze3d.vertex.PoseStack; -import dev.loki.lovisual.render.engine.postprocess.post.PostProcessPass; -import dev.loki.lovisual.util.screen.ClientScreen; -import net.minecraft.client.Camera; -import net.minecraft.client.DeltaTracker; -import net.minecraft.client.Minecraft; -import net.minecraft.client.player.AbstractClientPlayer; -import net.minecraft.client.player.LocalPlayer; -import net.minecraft.client.renderer.GameRenderer; -import net.minecraft.client.renderer.ProjectionMatrixBuffer; -import net.minecraft.client.renderer.SubmitNodeStorage; -import net.minecraft.client.renderer.culling.Frustum; -import net.minecraft.client.renderer.feature.FeatureRenderDispatcher; -import net.minecraft.client.renderer.state.GameRenderState; -import net.minecraft.client.renderer.state.level.CameraRenderState; -import net.minecraft.util.Mth; -import net.minecraft.world.effect.MobEffects; -import net.minecraft.world.level.material.FogType; -import net.minecraft.world.phys.Vec3; -import org.joml.Matrix4f; -import org.joml.Matrix4fc; -import org.joml.Vector3f; -import org.spongepowered.asm.mixin.Final; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.Unique; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.Redirect; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; -import dev.loki.lovisual.features.gui.hud.nondraggable.impl.buttons.BetterButtons; -import dev.loki.lovisual.addon.pipeline.AddonRenderPipelineManager; -import dev.loki.lovisual.api.v0.render.LoVisualRenderStage; -import dev.loki.lovisual.features.module.core.ModuleManager; -import dev.loki.lovisual.features.module.lifecycle.Modules; -import dev.loki.lovisual.features.module.phase.WorldPhase; -import dev.loki.lovisual.mixins.accessors.renderer.GameRendererAccessor; -import dev.loki.lovisual.mixins.accessors.entity.LocalPlayerAccessor; -import dev.loki.lovisual.render.engine.RenderState; -import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; -import dev.loki.lovisual.render.engine.core.system.LoVisualWorldMatrices; -import dev.loki.lovisual.render.engine.core.frame.RenderPhase; -import dev.loki.lovisual.render.engine.core.frame.RenderPhaseScope; -import dev.loki.lovisual.render.engine.depth.WorldSceneDepth; -// import dev.loki.lovisual.render.engine.debug.RenderThread2DDebugRenderer; // disabled: debug probes are manual-only -import dev.loki.lovisual.render.engine.msaa.MsaaWorldTarget; -import dev.loki.lovisual.config.i18n.MainConfig; -import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; -import dev.loki.lovisual.render.engine.postprocess.post.PostProcessManager; -import dev.loki.lovisual.render.engine.postprocess.render.MenuBackgroundRenderer; -import dev.loki.lovisual.render.engine.profiler.config.ProfilerPhase; -import dev.loki.lovisual.render.engine.profiler.render.RenderProfiler3D; -import dev.loki.lovisual.render.engine.profiler.bridge.TracyGpuProfiler; -import dev.loki.lovisual.render.engine.renderer.MeshRenderer; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.renderer.Renderer3D; -import dev.loki.lovisual.render.engine.text.render.TextRenderer; -import dev.loki.lovisual.render.engine.visuals.LoVisualVisuals; -import dev.loki.lovisual.render.iris.state.IrisLoVisualFrameHooks; -import dev.loki.lovisual.render.iris.scene.IrisFinalizedSceneRenderer; -import dev.loki.lovisual.render.iris.compat.IrisCompatibilityGuards; -import dev.loki.lovisual.render.iris.runtime.IrisRuntime; -import dev.loki.lovisual.util.logging.DebugLog; -import dev.loki.lovisual.features.module.modules.visuals.camera.AspectRatio; -import dev.loki.lovisual.features.module.modules.visuals.camera.Freecam; -import dev.loki.lovisual.features.module.modules.visuals.camera.ViewModel; -import dev.loki.lovisual.features.module.modules.visuals.scene.visual.Chams; -import dev.loki.lovisual.features.module.modules.visuals.scene.visual.ReimaginedVisual; -import dev.loki.lovisual.features.module.modules.visuals.visibility.NoRender; -import dev.loki.lovisual.render.engine.renderer.ui.deferred.sched.HudDeferredGuiElement; -import dev.loki.lovisual.features.module.modules.misc.ClickGui; - -@Mixin(GameRenderer.class) -public abstract class GameRendererMixin implements IrisFinalizedSceneRenderer { - - @Unique - private static final boolean LOVISUAL_DEBUG_PRIMITIVE = false; - @Unique - private static final boolean LOVISUAL_DEBUG_CLIP_SPACE = false; - @Unique - private static final long LOVISUAL_DEBUG_INTERVAL_MS = 2000L; - @Unique - private static long LOVISUAL_DEBUG_LAST_MS; - @Unique - private final PoseStack lovisual$matrices = new PoseStack(); - @Unique - private final PoseStack lovisual$postMatrices = new PoseStack(); - // Per-frame scratch (GameRenderer is a singleton; consumers copy by value): - // avoids ~7 Matrix4f/PoseStack/Quaternionf allocations per frame in the hot render path. - @Unique - private final PoseStack lovisual$effectMatrices = new PoseStack(); - @Unique - private final Matrix4f lovisual$screenProjectionScratch = new Matrix4f(); - @Unique - private final Matrix4f lovisual$modelViewScratch = new Matrix4f(); - @Unique - private final Matrix4f lovisual$inverseBobScratch = new Matrix4f(); - @Unique - private final Matrix4f lovisual$cameraPositionScratch = new Matrix4f(); - @Unique - private final org.joml.Quaternionf lovisual$conjugateScratch = new org.joml.Quaternionf(); - @Shadow - @Final - private Minecraft minecraft; - @Shadow - @Final - private Camera mainCamera; - @Shadow - @Final - private GameRenderState gameRenderState; - @Shadow - private float spinningEffectTime; - @Shadow - private float spinningEffectSpeed; - @Unique - private Renderer3D lovisual$renderer; - @Unique - private Renderer3D lovisual$depthRenderer; - @Unique - private Renderer3D lovisual$postRenderer; - @Unique - private Renderer3D lovisual$postDepthRenderer; - - @Unique - private static void lovisual$drawDebugCube(Renderer3D rdr, - double x1, double y1, double z1, - double x2, double y2, double z2) { - rdr.line(x1, y1, z1, x2, y1, z1, 255, 0, 0, 255); - rdr.line(x1, y2, z1, x2, y2, z1, 255, 0, 0, 255); - rdr.line(x1, y1, z2, x2, y1, z2, 255, 0, 0, 255); - rdr.line(x1, y2, z2, x2, y2, z2, 255, 0, 0, 255); - - rdr.line(x1, y1, z1, x1, y2, z1, 0, 255, 0, 255); - rdr.line(x2, y1, z1, x2, y2, z1, 0, 255, 0, 255); - rdr.line(x1, y1, z2, x1, y2, z2, 0, 255, 0, 255); - rdr.line(x2, y1, z2, x2, y2, z2, 0, 255, 0, 255); - - rdr.line(x1, y1, z1, x1, y1, z2, 0, 0, 255, 255); - rdr.line(x2, y1, z1, x2, y1, z2, 0, 0, 255, 255); - rdr.line(x1, y2, z1, x1, y2, z2, 0, 0, 255, 255); - rdr.line(x2, y2, z1, x2, y2, z2, 0, 0, 255, 255); - } - - @Unique - private static boolean isWorldTranslucent(FogType type) { - return type == FogType.WATER - || type == FogType.LAVA - || type == FogType.POWDER_SNOW; - } - - @Shadow - protected abstract void bobView(CameraRenderState cameraRenderState, PoseStack matrices); - - @Shadow - protected abstract void bobHurt(CameraRenderState cameraRenderState, PoseStack matrices); - - @Unique - private static boolean lovisual$needsResolvedMainDepth() { - ReimaginedVisual module = Modules.get(ReimaginedVisual.class); - if (module == null) return false; - if (module.needsResolvedMainDepthCapture()) return true; - return MainConfig.get().getMsaa3dSamples() > 1 && module.isActive(); - } - - @Inject(method = "tick", at = @At("HEAD"), cancellable = true) - private void lovisual$skipTickWithoutPlayer(CallbackInfo ci) { - if (minecraft != null && minecraft.level != null && minecraft.player == null) { - ci.cancel(); - } - } - - @Inject(method = "extract(Lnet/minecraft/client/DeltaTracker;Z)V", at = @At("HEAD")) - private void lovisual$beginDeferred2DExtract(DeltaTracker tickCounter, boolean tick, CallbackInfo ci) { - Renderer2D.beginDeferredExtractFrame(); - } - - @Inject(method = "extract(Lnet/minecraft/client/DeltaTracker;Z)V", at = @At("TAIL")) - private void lovisual$endDeferred2DExtract(DeltaTracker tickCounter, boolean tick, CallbackInfo ci) { - Renderer2D.endDeferredExtractFrame(); - } - - @Redirect( - method = "renderItemInHand(Lnet/minecraft/client/renderer/state/level/CameraRenderState;FLorg/joml/Matrix4fc;)V", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/client/renderer/GameRenderer;bobView(Lnet/minecraft/client/renderer/state/level/CameraRenderState;Lcom/mojang/blaze3d/vertex/PoseStack;)V" - ) - ) - private void lovisual$viewModel$replaceHandBobbing(GameRenderer instance, - CameraRenderState cameraRenderState, - PoseStack matrices, - CameraRenderState originalCameraRenderState, - float tickDelta, - Matrix4fc positionMatrix) { - ViewModel viewModel = Modules.get(ViewModel.class); - if (viewModel != null && viewModel.smoothRunLoweringEnabled()) { - if (minecraft != null && minecraft.getCameraEntity() instanceof AbstractClientPlayer player) { - float movement = player.avatarState().getInterpolatedBob(tickDelta); - float yOffset = -Mth.clamp(movement, 0.0f, 1.0f) * viewModel.getSmoothRunLoweringAmount(); - if (Math.abs(yOffset) > 0.0001f) { - matrices.translate(0.0f, yOffset, 0.0f); - } - } - return; - } - - ((GameRendererAccessor) instance).invokeBobView(cameraRenderState, matrices); - } - - @Inject(method = "extractCamera", at = @At("TAIL")) - private void lovisual$modifyAspect(DeltaTracker tickCounter, float tickDelta, float fov, CallbackInfo ci) { - - AspectRatio ar = Modules.get(AspectRatio.class); - if (ar == null || !ar.isEnabled()) { - return; - } - - float mul = ar.getMultiplier(); - if (mul == 1.0f) { - return; - } - - Matrix4f m = gameRenderState.levelRenderState.cameraRenderState.projectionMatrix; - if (m == null) return; - - // horizontal aspect stretch - m.m00(m.m00() * mul); - } - - @Inject(method = "extractCamera", at = @At("TAIL")) - private void lovisual$captureProjectionFor2DOverlays(DeltaTracker tickCounter, float tickDelta, float fov, CallbackInfo ci) { - if (gameRenderState == null || gameRenderState.levelRenderState == null) return; - CameraRenderState cameraRenderState = gameRenderState.levelRenderState.cameraRenderState; - if (cameraRenderState == null || cameraRenderState.viewRotationMatrix == null || cameraRenderState.projectionMatrix == null) return; - - lovisual$effectMatrices.setIdentity(); - Matrix4f renderProjection = lovisual$buildWorldProjection(fov, lovisual$effectMatrices, tickDelta); - AspectRatio.applyToProjection(renderProjection); - Matrix4f screenProjection = lovisual$screenProjectionScratch.set(cameraRenderState.projectionMatrix); - LoVisualWorldMatrices.capture( - cameraRenderState.viewRotationMatrix, - renderProjection, - screenProjection, - cameraRenderState.pos != null ? cameraRenderState.pos : mainCamera.position() - ); - } - - @Inject(method = "mainRenderTarget", at = @At("HEAD"), cancellable = true) - private void lovisual$overrideWorldFramebuffer(CallbackInfoReturnable cir) { - com.mojang.blaze3d.pipeline.RenderTarget msaa = MsaaWorldTarget.getFramebufferOverride(); - if (msaa != null) { - cir.setReturnValue(msaa); - } - } - - @Unique - private void lovisual$renderWorldEngine(DeltaTracker tickCounter) { - try (ProfilerPhase.Scope phaseScope = ProfilerPhase.scope("3d:world"); - TracyGpuProfiler.Scope gpuScope = TracyGpuProfiler.beginZone("3d:world")) { - float tickDelta = tickCounter.getGameTimeDeltaPartialTick(true); - float fov = mainCamera.getFov(); - Matrix4f capturedPosition = LoVisualWorldMatrices.positionMatrix(); - Matrix4f capturedProjection = LoVisualWorldMatrices.renderProjectionMatrix(); - boolean useCapturedMatrices = capturedPosition != null && capturedProjection != null; - Matrix4f position = useCapturedMatrices - ? capturedPosition - : lovisual$cameraPositionScratch.identity().rotation( - mainCamera.rotation().conjugate(lovisual$conjugateScratch)); - RenderState.rendering3D = true; - RenderState.tickDelta = tickDelta; - Vec3 capturedCameraPos = LoVisualWorldMatrices.cameraPosition(); - RenderState.cameraPos = capturedCameraPos != null ? capturedCameraPos : mainCamera.position(); - RenderState.cameraRotation.set(mainCamera.rotation()); - RenderState.cameraYaw = mainCamera.yRot(); - RenderState.cameraPitch = mainCamera.xRot(); - RenderState.cameraLook = Vec3.directionFromRotation(RenderState.cameraPitch, RenderState.cameraYaw).normalize(); - RenderState.cameraSubmersion = mainCamera.getFluidInCamera(); - RenderState.worldTranslucent = isWorldTranslucent(RenderState.cameraSubmersion); - RenderState.cameraLook = Vec3.directionFromRotation(RenderState.cameraPitch, RenderState.cameraYaw).normalize(); - RenderState.cameraSubmersion = mainCamera.getFluidInCamera(); - RenderState.worldTranslucent = isWorldTranslucent(RenderState.cameraSubmersion); - RenderState.frustum = mainCamera != null - ? mainCamera.getCapturedFrustum() - : null; - - if (LOVISUAL_DEBUG_CLIP_SPACE) { - var mv = com.mojang.blaze3d.systems.RenderSystem.getModelViewStack(); - mv.pushMatrix(); - mv.identity(); - - MeshRenderer.setProjection(new Matrix4f().identity()); - RenderState.rendering3D = false; - - if (lovisual$renderer == null) { - lovisual$renderer = new Renderer3D( - LoVisualRenderPipelines.WORLD_COLORED_LINES, - LoVisualRenderPipelines.WORLD_COLORED - ); - } - - lovisual$renderer.begin(); - - // Clip-space square + diagonals (NDC) - lovisual$renderer.line(-0.8, -0.8, 0.0, 0.8, -0.8, 0.0, 255, 0, 0, 255); - lovisual$renderer.line(0.8, -0.8, 0.0, 0.8, 0.8, 0.0, 0, 255, 0, 255); - lovisual$renderer.line(0.8, 0.8, 0.0, -0.8, 0.8, 0.0, 0, 0, 255, 255); - lovisual$renderer.line(-0.8, 0.8, 0.0, -0.8, -0.8, 0.0, 255, 255, 0, 255); - lovisual$renderer.line(-0.8, -0.8, 0.0, 0.8, 0.8, 0.0, 255, 0, 255, 255); - lovisual$renderer.line(0.8, -0.8, 0.0, -0.8, 0.8, 0.0, 0, 255, 255, 255); - - lovisual$renderer.render(new PoseStack()); - - mv.popMatrix(); - RenderState.rendering3D = false; - return; - } - - if (lovisual$renderer == null) { - lovisual$renderer = new Renderer3D( - LoVisualRenderPipelines.WORLD_COLORED_LINES, - LoVisualRenderPipelines.WORLD_COLORED - ); - } - - if (lovisual$depthRenderer == null) { - lovisual$depthRenderer = new Renderer3D( - LoVisualRenderPipelines.WORLD_COLORED_LINES_DEPTH, - LoVisualRenderPipelines.WORLD_COLORED_DEPTH - ); - } - - var mv = com.mojang.blaze3d.systems.RenderSystem.getModelViewStack(); - mv.pushMatrix(); - if (IrisLoVisualFrameHooks.isRenderingAfterIrisFinalization()) { - mv.identity(); - } - mv.mul(position); - lovisual$matrices.pushPose(); - Matrix4f projection = useCapturedMatrices - ? capturedProjection - : lovisual$buildWorldProjection(fov, lovisual$matrices, tickDelta); - MeshRenderer.setProjection(projection); - LoVisualRenderSystem.beginFrame( - tickDelta, - tickCounter.getGameTimeDeltaTicks(), - tickCounter.getRealtimeDeltaTicks(), - projection, - lovisual$modelViewScratch.set(mv) - ); - try (RenderPhaseScope lovisual$beforeTranslucentPhase = LoVisualRenderSystem.phase(RenderPhase.WORLD_BEFORE_TRANSLUCENT)) { - RenderState.worldProjection.set(projection); - RenderState.frustum = new Frustum(position, projection); - RenderState.frustum.prepare(RenderState.cameraPos.x, RenderState.cameraPos.y, RenderState.cameraPos.z); - - if (!useCapturedMatrices) { - mv.mul(lovisual$inverseBobScratch.set(lovisual$matrices.last().pose()).invert()); - } - - lovisual$renderer.begin(); - lovisual$depthRenderer.begin(); - - if (LOVISUAL_DEBUG_PRIMITIVE) { - Vec3 camPos = mainCamera.position(); - float yaw = mainCamera.yRot() * ((float) Math.PI / 180.0f); - float pitch = mainCamera.xRot() * ((float) Math.PI / 180.0f); - Vec3 forward = new Vec3( - -Mth.sin(yaw) * Mth.cos(pitch), - -Mth.sin(pitch), - Mth.cos(yaw) * Mth.cos(pitch) - ); - - Vec3 center = camPos.add(forward.scale(3.0)); - double r = 0.5; - double x1 = center.x - r, x2 = center.x + r; - double y1 = center.y - r, y2 = center.y + r; - double z1 = center.z - r, z2 = center.z + r; - double ox1 = -1.0, oy1 = -1.0, oz1 = -1.0; - double ox2 = 1.0, oy2 = 1.0, oz2 = 1.0; - - // Wire cube (no depth) and depth variant - Renderer3D[] targets = new Renderer3D[]{lovisual$renderer, lovisual$depthRenderer}; - for (Renderer3D rdr : targets) { - lovisual$drawDebugCube(rdr, x1, y1, z1, x2, y2, z2); - lovisual$drawDebugCube(rdr, ox1, oy1, oz1, ox2, oy2, oz2); - } - - long now = System.currentTimeMillis(); - if (now - LOVISUAL_DEBUG_LAST_MS >= LOVISUAL_DEBUG_INTERVAL_MS) { - LOVISUAL_DEBUG_LAST_MS = now; - Matrix4f mvMat = com.mojang.blaze3d.systems.RenderSystem.getModelViewMatrixCopy(); - DebugLog.renderThread( - "[LoVisual] Debug diag: cam=" + camPos - + " proj=[" + projection.m00() + "," + projection.m11() + "," + projection.m22() + "," + projection.m33() + "]" - + " mv=[" + mvMat.m30() + "," + mvMat.m31() + "," + mvMat.m32() + "]" - ); - } - } - - try (ProfilerPhase.Scope phase = ProfilerPhase.scope("3d:modules_before_translucent"); - RenderProfiler3D.Section ignored = RenderProfiler3D.section("modules_before_translucent")) { - ModuleManager.renderWorldEngine(WorldPhase.BEFORE_TRANSLUCENT, lovisual$renderer, lovisual$depthRenderer, tickDelta); - AddonRenderPipelineManager.render3D(LoVisualRenderStage.WORLD_BEFORE_TRANSLUCENT, - WorldPhase.BEFORE_TRANSLUCENT, lovisual$renderer, lovisual$depthRenderer, lovisual$matrices, tickDelta); - } - - try (ProfilerPhase.Scope phase = ProfilerPhase.scope("3d:flush_before_translucent"); - TracyGpuProfiler.Scope gpuFlush = TracyGpuProfiler.beginZone("3d:flush_before_translucent"); - RenderProfiler3D.Section ignored = RenderProfiler3D.section("flush_before_translucent")) { - lovisual$renderer.render(lovisual$matrices); - lovisual$depthRenderer.render(lovisual$matrices); - } - } - - // Start a new batch for END_MAIN so overlays render above grass. - lovisual$renderer.begin(); - lovisual$depthRenderer.begin(); - - try (RenderPhaseScope lovisual$afterTranslucentPhase = LoVisualRenderSystem.phase(RenderPhase.WORLD_AFTER_TRANSLUCENT)) { - try (ProfilerPhase.Scope phase = ProfilerPhase.scope("3d:modules_end_main"); - RenderProfiler3D.Section ignored = RenderProfiler3D.section("modules_end_main")) { - ModuleManager.renderWorldEngine(WorldPhase.END_MAIN, lovisual$renderer, lovisual$depthRenderer, tickDelta); - AddonRenderPipelineManager.render3D(LoVisualRenderStage.WORLD_END_MAIN, - WorldPhase.END_MAIN, lovisual$renderer, lovisual$depthRenderer, lovisual$matrices, tickDelta); - } - - try (ProfilerPhase.Scope phase = ProfilerPhase.scope("3d:flush_end_main"); - TracyGpuProfiler.Scope gpuFlush = TracyGpuProfiler.beginZone("3d:flush_end_main"); - RenderProfiler3D.Section ignored = RenderProfiler3D.section("flush_end_main")) { - lovisual$renderer.render(lovisual$matrices); - lovisual$depthRenderer.render(lovisual$matrices); - } - } - lovisual$matrices.popPose(); - mv.popMatrix(); - RenderState.rendering3D = false; - } - } - - @Inject(method = "renderLevel", at = @At("HEAD")) - private void lovisual$beginWorldMsaa(DeltaTracker tickCounter, CallbackInfo ci) { - IrisLoVisualFrameHooks.beginRenderLevel(tickCounter); - LoVisualWorldMatrices.reset(); - int samples = MainConfig.get().getMsaa3dSamples(); - MsaaWorldTarget.begin(minecraft, samples, lovisual$needsResolvedMainDepth()); - } - - @Inject(method = "renderLevel", at = @At("RETURN")) - private void lovisual$endIrisFrameHooks(DeltaTracker tickCounter, CallbackInfo ci) { - IrisLoVisualFrameHooks.endRenderLevel(tickCounter); - } - - @WrapOperation( - method = "renderLevel", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/client/renderer/ProjectionMatrixBuffer;getBuffer(Lorg/joml/Matrix4f;)Lcom/mojang/blaze3d/buffers/GpuBufferSlice;" - ) - ) - private GpuBufferSlice lovisual$captureFullWorldMatrices(ProjectionMatrixBuffer instance, - Matrix4f renderProjectionMatrix, - Operation original) { - if (renderProjectionMatrix != null - && gameRenderState != null - && gameRenderState.levelRenderState != null - && gameRenderState.levelRenderState.cameraRenderState != null - && gameRenderState.levelRenderState.cameraRenderState.viewRotationMatrix != null - && gameRenderState.levelRenderState.cameraRenderState.projectionMatrix != null) { - CameraRenderState cameraRenderState = gameRenderState.levelRenderState.cameraRenderState; - LoVisualWorldMatrices.capture( - cameraRenderState.viewRotationMatrix, - renderProjectionMatrix, - cameraRenderState.projectionMatrix, - cameraRenderState.pos != null ? cameraRenderState.pos : mainCamera.position() - ); - } - return original.call(instance, renderProjectionMatrix); - } - - @Inject( - method = "renderLevel", - at = @At( - value = "INVOKE_STRING", - target = "Lnet/minecraft/util/profiling/ProfilerFiller;popPush(Ljava/lang/String;)V", - args = "ldc=hand", - shift = At.Shift.AFTER - ) - ) - private void lovisual$postProcess(DeltaTracker tickCounter, CallbackInfo ci) { - if (IrisRuntime.isShaderpackRendererActive() - && IrisCompatibilityGuards.deferIrisFinalizationForSecondHandScene()) { - return; - } - lovisual$renderPreHandPostProcess(tickCounter); - } - - @Unique - private void lovisual$renderPreHandPostProcess(DeltaTracker tickCounter) { - ProfilerPhase.begin("3d:post_pre_hand"); - try (TracyGpuProfiler.Scope gpuScope = TracyGpuProfiler.beginZone("3d:post_pre_hand")) { - if (minecraft != null) { - boolean needsResolvedDepth = !IrisRuntime.isShaderpackRendererActive() - && lovisual$needsResolvedMainDepth(); - boolean capturedMsaaDepth = needsResolvedDepth - && MsaaWorldTarget.isActive() - && WorldSceneDepth.captureResolvedMain(MsaaWorldTarget.getMsaaFramebuffer()); - MsaaWorldTarget.resolveToMain(minecraft); - if (needsResolvedDepth && !capturedMsaaDepth) { - WorldSceneDepth.captureResolvedMain(minecraft.gameRenderer.mainRenderTarget()); - } - LoVisualVisuals.renderWorldBase(minecraft, tickCounter.getGameTimeDeltaPartialTick(true)); - } - try (RenderPhaseScope lovisual$postPreHandPhase = LoVisualRenderSystem.phase(RenderPhase.WORLD_POST_PRE_HAND)) { - PostProcessManager.renderAll(PostProcessPass.Phase.PRE_HAND, - tickCounter.getGameTimeDeltaPartialTick(true)); - lovisual$renderLoVisualWorldAfterPreHandPostProcess(tickCounter); - } - } - ProfilerPhase.end("3d:post_pre_hand"); - } - - @Unique - private void lovisual$renderLoVisualWorldAfterPreHandPostProcess(DeltaTracker tickCounter) { - if (minecraft == null) { - lovisual$renderWorldEngine(tickCounter); - lovisual$renderPostProcessWorld(tickCounter); - return; - } - - MsaaWorldTarget.begin(minecraft, MainConfig.get().getMsaa3dSamples(), false, true); - try { - lovisual$renderWorldEngine(tickCounter); - lovisual$renderPostProcessWorld(tickCounter); - } finally { - MsaaWorldTarget.resolveToMain(minecraft); - } - } - - @Override - public void lovisual$renderAfterIrisFinalization(DeltaTracker tickCounter) { - lovisual$renderPreHandPostProcess(tickCounter); - } - - @Inject( - method = "renderLevel", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/client/renderer/feature/FeatureRenderDispatcher;renderAllFeatures(Lnet/minecraft/client/renderer/SubmitNodeStorage;)V", - shift = At.Shift.AFTER - ) - ) - private void lovisual$postProcessAfterHand(DeltaTracker tickCounter, CallbackInfo ci) { - ProfilerPhase.begin("3d:post_after_hand"); - try (TracyGpuProfiler.Scope gpuScope = TracyGpuProfiler.beginZone("3d:post_after_hand")) { - if (minecraft != null) { - MsaaWorldTarget.resolveToMain(minecraft); - } - try (RenderPhaseScope lovisual$postHandPhase = LoVisualRenderSystem.phase(RenderPhase.WORLD_POST_HAND)) { - PostProcessManager.renderAll(PostProcessPass.Phase.POST_HAND, - tickCounter.getGameTimeDeltaPartialTick(true)); - } - } - ProfilerPhase.end("3d:post_after_hand"); - } - - @Unique - private void lovisual$renderPostProcessWorld(DeltaTracker tickCounter) { - try (ProfilerPhase.Scope phaseScope = ProfilerPhase.scope("3d:world_post"); - TracyGpuProfiler.Scope gpuScope = TracyGpuProfiler.beginZone("3d:world_post")) { - float tickDelta = tickCounter.getGameTimeDeltaPartialTick(true); - float fov = mainCamera.getFov(); - Matrix4f capturedPosition = LoVisualWorldMatrices.positionMatrix(); - Matrix4f capturedProjection = LoVisualWorldMatrices.renderProjectionMatrix(); - boolean useCapturedMatrices = capturedPosition != null && capturedProjection != null; - Matrix4f position = useCapturedMatrices - ? capturedPosition - : lovisual$cameraPositionScratch.identity().rotation( - mainCamera.rotation().conjugate(lovisual$conjugateScratch)); - - RenderState.rendering3D = true; - RenderState.tickDelta = tickDelta; - Vec3 capturedCameraPos = LoVisualWorldMatrices.cameraPosition(); - RenderState.cameraPos = capturedCameraPos != null ? capturedCameraPos : mainCamera.position(); - RenderState.cameraRotation.set(mainCamera.rotation()); - RenderState.cameraYaw = mainCamera.yRot(); - RenderState.cameraPitch = mainCamera.xRot(); - - if (lovisual$postRenderer == null) { - lovisual$postRenderer = new Renderer3D( - LoVisualRenderPipelines.WORLD_COLORED_LINES, - LoVisualRenderPipelines.WORLD_COLORED - ); - } - - if (lovisual$postDepthRenderer == null) { - lovisual$postDepthRenderer = new Renderer3D( - LoVisualRenderPipelines.WORLD_COLORED_LINES_DEPTH, - LoVisualRenderPipelines.WORLD_COLORED_DEPTH - ); - } - - var mv = com.mojang.blaze3d.systems.RenderSystem.getModelViewStack(); - mv.pushMatrix(); - if (IrisLoVisualFrameHooks.isRenderingAfterIrisFinalization()) { - mv.identity(); - } - mv.mul(position); - lovisual$postMatrices.pushPose(); - Matrix4f projection = useCapturedMatrices - ? capturedProjection - : lovisual$buildWorldProjection(fov, lovisual$postMatrices, tickDelta); - MeshRenderer.setProjection(projection); - LoVisualRenderSystem.beginFrame( - tickDelta, - tickCounter.getGameTimeDeltaTicks(), - tickCounter.getRealtimeDeltaTicks(), - projection, - lovisual$modelViewScratch.set(mv) - ); - try (RenderPhaseScope lovisual$postWorldPhase = LoVisualRenderSystem.phase(RenderPhase.WORLD_POST_PRE_HAND)) { - RenderState.worldProjection.set(projection); - RenderState.frustum = new Frustum(position, projection); - RenderState.frustum.prepare(RenderState.cameraPos.x, RenderState.cameraPos.y, RenderState.cameraPos.z); - - if (!useCapturedMatrices) { - mv.mul(lovisual$inverseBobScratch.set(lovisual$postMatrices.last().pose()).invert()); - } - - lovisual$postRenderer.begin(); - lovisual$postDepthRenderer.begin(); - - try (ProfilerPhase.Scope phase = ProfilerPhase.scope("3d:post_modules"); - RenderProfiler3D.Section ignored = RenderProfiler3D.section("modules")) { - ModuleManager.renderWorldEngine(WorldPhase.AFTER_POST_PROCESS, lovisual$postRenderer, lovisual$postDepthRenderer, tickDelta); - AddonRenderPipelineManager.render3D(LoVisualRenderStage.WORLD_POST_PROCESS, - WorldPhase.AFTER_POST_PROCESS, lovisual$postRenderer, lovisual$postDepthRenderer, lovisual$postMatrices, tickDelta); - } - - try (ProfilerPhase.Scope phase = ProfilerPhase.scope("3d:post_flush"); - TracyGpuProfiler.Scope gpuFlush = TracyGpuProfiler.beginZone("3d:post_flush"); - RenderProfiler3D.Section ignored = RenderProfiler3D.section("flush")) { - lovisual$postRenderer.render(lovisual$postMatrices); - lovisual$postDepthRenderer.render(lovisual$postMatrices); - } - } - lovisual$postMatrices.popPose(); - mv.popMatrix(); - RenderState.rendering3D = false; - } - } - - @Unique - private Matrix4f lovisual$buildWorldProjection(float fov, PoseStack viewEffectMatrices, float tickDelta) { - CameraRenderState cameraRenderState = new CameraRenderState(); - mainCamera.extractRenderState(cameraRenderState, fov); - Matrix4f projection = new Matrix4f(cameraRenderState.projectionMatrix); - lovisual$applyViewEffectMatrices(viewEffectMatrices, tickDelta); - projection.mul(viewEffectMatrices.last().pose()); - lovisual$applyNauseaProjection(projection, tickDelta); - return projection; - } - - @Unique - private void lovisual$applyViewEffectMatrices(PoseStack matrices, float cameraTickProgress) { - NoRender noRender = Modules.get(NoRender.class); - CameraRenderState cameraRenderState = new CameraRenderState(); - mainCamera.extractRenderState(cameraRenderState, mainCamera.getFov()); - if (noRender == null || !noRender.off("camera_shake")) { - bobHurt(cameraRenderState, matrices); - } - - boolean freecamEnabled = Modules.get(Freecam.class) != null && Modules.get(Freecam.class).isEnabled(); - if (minecraft.options.bobView().get() - && !freecamEnabled - && (noRender == null || !noRender.off("view_bob"))) { - bobView(cameraRenderState, matrices); - } - } - - @Unique - private void lovisual$applyNauseaProjection(Matrix4f projection, float tickDelta) { - LocalPlayer player = minecraft.player; - if (player == null) return; - - float distortionScale = minecraft.options.screenEffectScale().get().floatValue(); - if (distortionScale <= 0.0f) return; - - float nauseaIntensity = 0.0f; - if (player instanceof LocalPlayerAccessor accessor) { - nauseaIntensity = Mth.lerp( - tickDelta, - accessor.lovisual$getLastNauseaIntensity(), - accessor.lovisual$getNauseaIntensity() - ); - } - - float nauseaEffect = player.getEffectBlendFactor(MobEffects.NAUSEA, tickDelta); - float strength = Math.max(nauseaIntensity, nauseaEffect) * (distortionScale * distortionScale); - if (strength <= 0.0f) return; - - float scale = 5.0f / (strength * strength + 5.0f) - strength * 0.04f; - scale *= scale; - Vector3f axis = new Vector3f(0.0f, Mth.SQRT_OF_TWO / 2.0f, Mth.SQRT_OF_TWO / 2.0f); - float angle = (this.spinningEffectTime + tickDelta * this.spinningEffectSpeed) * Mth.DEG_TO_RAD; - projection.rotate(angle, axis); - projection.scale(1.0f / scale, 1.0f, 1.0f); - projection.rotate(-angle, axis); - } - - @Inject(method = "renderItemInHand(Lnet/minecraft/client/renderer/state/level/CameraRenderState;FLorg/joml/Matrix4fc;)V", at = @At("HEAD"), cancellable = true) - private void freecam$hand(CameraRenderState cameraRenderState, float tickDelta, Matrix4fc positionMatrix, CallbackInfo ci) { - Freecam fc = Modules.get(Freecam.class); - if (fc != null && fc.isEnabled() && !fc.renderHand()) { - ci.cancel(); - } - } - - @WrapOperation( - method = "renderItemInHand(Lnet/minecraft/client/renderer/state/level/CameraRenderState;FLorg/joml/Matrix4fc;)V", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/client/renderer/feature/FeatureRenderDispatcher;renderAllFeatures(Lnet/minecraft/client/renderer/SubmitNodeStorage;)V" - ) - ) - private void lovisual$renderChamsHandPreparedFrame(FeatureRenderDispatcher dispatcher, - SubmitNodeStorage storage, - Operation original, - CameraRenderState cameraRenderState, - float tickDelta, - Matrix4fc positionMatrix) { - Chams module = Modules.get(Chams.class); - if (module != null && module.renderPreparedHandScene(dispatcher, storage)) { - return; - } - original.call(dispatcher, storage); - } - - @Inject(method = "render(Lnet/minecraft/client/DeltaTracker;Z)V", at = @At("TAIL")) - private void lovisual$flushQueuedButtons(DeltaTracker tickCounter, boolean tick, CallbackInfo ci) { - Minecraft mc = Minecraft.getInstance(); - if (mc == null || ClientScreen.current() == null) return; - if (!BetterButtons.hasPending()) { - BetterButtons.renderTooltip(); - return; - } - BetterButtons.flush(); - BetterButtons.renderTooltip(); - } - - @Inject( - method = "render(Lnet/minecraft/client/DeltaTracker;Z)V", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/client/gui/render/GuiRenderer;render()V", - shift = At.Shift.BEFORE - ) - ) - private void lovisual$captureWorldForHudGlass(DeltaTracker tickCounter, boolean tick, CallbackInfo ci) { - MenuBackgroundRenderer.drainDeferred(minecraft); - Renderer2D.captureWorldGlassSource(); - if (AddonRenderPipelineManager.hasActiveCallbacks(LoVisualRenderStage.SCREEN_BEFORE_VANILLA_GUI)) { - float tickDelta = tickCounter.getGameTimeDeltaPartialTick(true); - Renderer2D.COLOR.begin(); - AddonRenderPipelineManager.render2D(LoVisualRenderStage.SCREEN_BEFORE_VANILLA_GUI, - null, Renderer2D.COLOR, TextRenderer.get(), null, tickDelta); - Renderer2D.COLOR.render(); - } - Renderer2D.drainDeferred2D(Renderer2D.Deferred2DLayer.BEFORE_VANILLA_GUI); - lovisual$drainHudLayersBelowScreenGui(); - } - - /** - * Gui.extractRenderState() extracts vanilla HUD first and the current Screen after it. - * Normal gameplay keeps LoVisual HUD spliced into that HUD order through HudDeferredGuiElement - * markers. When a real Screen is open, however, the screen must remain the top GUI owner: - * HUD/world overlaysare drained before GuiRenderer.render(), - * preserving their internal order while keeping container/inventory GUI above them. - * Explicit SCREEN_TOP/AFTER_VANILLA_GUI overlays (Dynamic Island, ClickGui top layer, etc.) are - * intentionally not drained here. - */ - @Unique - private static void lovisual$drainHudLayersBelowScreenGui() { - if (ClientScreen.current() == null) { - return; - } - - Renderer2D.drainDeferred2D(Renderer2D.Deferred2DLayer.HUD_FIRST); - Renderer2D.drainDeferred2D(Renderer2D.Deferred2DLayer.HUD_BEFORE_MISC_OVERLAYS); - Renderer2D.drainDeferred2D(Renderer2D.Deferred2DLayer.HUD_AFTER_MISC_OVERLAYS); - Renderer2D.drainDeferred2D(Renderer2D.Deferred2DLayer.HUD_AFTER_BOSS_BAR); - Renderer2D.drainDeferred2D(Renderer2D.Deferred2DLayer.HUD_BEFORE_DEMO_TIMER); - Renderer2D.drainDeferred2D(Renderer2D.Deferred2DLayer.HUD_BEFORE_CHAT); - Renderer2D.drainDeferred2D(Renderer2D.Deferred2DLayer.HUD_AFTER_SUBTITLES); - Renderer2D.drainDeferred2D(Renderer2D.Deferred2DLayer.HUD_LAST); - } - - @Inject( - method = "render(Lnet/minecraft/client/DeltaTracker;Z)V", - at = @At( - value = "INVOKE", - target = "Lnet/minecraft/client/gui/render/GuiRenderer;render()V", - shift = At.Shift.AFTER - ) - ) - private void lovisual$debug2dImmediateAfterGui(DeltaTracker tickCounter, boolean tick, CallbackInfo ci) { - Renderer2D.drainDeferred2D(Renderer2D.Deferred2DLayer.AFTER_VANILLA_GUI); - if (AddonRenderPipelineManager.hasActiveCallbacks(LoVisualRenderStage.SCREEN_AFTER_VANILLA_GUI)) { - float tickDelta = tickCounter.getGameTimeDeltaPartialTick(true); - Renderer2D.COLOR.begin(); - AddonRenderPipelineManager.render2D(LoVisualRenderStage.SCREEN_AFTER_VANILLA_GUI, - null, Renderer2D.COLOR, TextRenderer.get(), null, tickDelta); - Renderer2D.COLOR.render(); - } - // Debug 2D probe intentionally disabled after projection validation. - // RenderThread2DDebugRenderer.renderImmediateAfterGui(tickCounter); - } - -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/UiMeshGeometry.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/UiMeshGeometry.java deleted file mode 100644 index 058de50..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/UiMeshGeometry.java +++ /dev/null @@ -1,956 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.render.engine.renderer.ui.draw.edge; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; - - -import dev.loki.lovisual.render.engine.uniform.MeshBuilder; - -/** - * Low-level mesh tessellation, gradient and path geometry used by Renderer2D. - * Kept outside the public drawing facade so API methods remain easy to navigate. - */ -public final class UiMeshGeometry { - private UiMeshGeometry() { - } - - public static void computeLinearGradientColors(float width, float height, - int startArgb, int endArgb, - float angleDeg, float offsetPx, - int[] out) { - float angle = (float) Math.toRadians(angleDeg); - float dirX = (float) Math.cos(angle); - float dirY = (float) Math.sin(angle); - - float p0 = 0.0f; - float p1 = width * dirX; - float p2 = height * dirY; - float p3 = width * dirX + height * dirY; - - float minProj = Math.min(Math.min(p0, p1), Math.min(p2, p3)); - float maxProj = Math.max(Math.max(p0, p1), Math.max(p2, p3)); - float range = Math.max(0.0001f, maxProj - minProj); - - float t0 = clamp01((p0 + offsetPx - minProj) / range); - float t1 = clamp01((p1 + offsetPx - minProj) / range); - float t2 = clamp01((p3 + offsetPx - minProj) / range); - float t3 = clamp01((p2 + offsetPx - minProj) / range); - - out[0] = mixArgb(startArgb, endArgb, t0); - out[1] = mixArgb(startArgb, endArgb, t1); - out[2] = mixArgb(startArgb, endArgb, t2); - out[3] = mixArgb(startArgb, endArgb, t3); - } - - private static int linearGradientColorAt(double x, double y, double width, double height, - int startArgb, int endArgb, float angleDeg, float offsetPx) { - float angle = (float) Math.toRadians(angleDeg); - float dirX = (float) Math.cos(angle); - float dirY = (float) Math.sin(angle); - - float w = (float) width; - float h = (float) height; - float p0 = 0.0f; - float p1 = w * dirX; - float p2 = h * dirY; - float p3 = w * dirX + h * dirY; - float minProj = Math.min(Math.min(p0, p1), Math.min(p2, p3)); - float maxProj = Math.max(Math.max(p0, p1), Math.max(p2, p3)); - float range = Math.max(0.0001f, maxProj - minProj); - float p = (float) x * dirX + (float) y * dirY; - return mixArgb(startArgb, endArgb, clamp01((p + offsetPx - minProj) / range)); - } - - public static float clamp01(float v) { - if (v < 0.0f) return 0.0f; - if (v > 1.0f) return 1.0f; - return v; - } - - public static float packLiquidGlassPayload(float distortPx, float squirclePower, float blurAlpha) { - float distort = Math.max(0.0f, Math.min(0.35f, distortPx)); - float smoothness = squirclePower < 1.5f - ? 2.0f - : Math.max(2.0f, Math.min(15.0f, Math.round(squirclePower))); - float blurBucket = Math.round(clamp01(blurAlpha) * 100.0f); - return smoothness * 10000.0f + blurBucket + distort; - } - - public static float clampRoundedRadius(float radius, double width, double height) { - float maxRadius = maxRoundedRadius(width, height); - if (radius < 0.0f) return 0.0f; - return Math.min(radius, maxRadius); - } - - public static float clampChamfer(double chamfer, double width, double height) { - float max = maxRoundedRadius(width, height); - if (chamfer < 0.0) return 0.0f; - return (float) Math.min(chamfer, max); - } - - public static float clampChamferAxis(double chamfer, double length) { - double max = Math.abs(length); - if (chamfer < 0.0 || max <= 0.0) return 0.0f; - return (float) Math.min(chamfer, max); - } - - private static float maxRoundedRadius(double width, double height) { - double w = Math.abs(width); - double h = Math.abs(height); - if (w <= 0.0 || h <= 0.0) { - return 0.0f; - } - return (float) (Math.min(w, h) * 0.5); - } - - public static void normalizeCornerRadii(double width, double height, - float radiusTL, float radiusTR, float radiusBR, float radiusBL, - float[] out) { - if (out == null || out.length < 4) { - throw new IllegalArgumentException("Output array must have length >= 4"); - } - - float w = (float) Math.abs(width); - float h = (float) Math.abs(height); - if (w <= 0.0f || h <= 0.0f) { - out[0] = 0.0f; - out[1] = 0.0f; - out[2] = 0.0f; - out[3] = 0.0f; - return; - } - - float maxRadius = Math.min(w, h) * 0.5f; - float tl = Math.max(0.0f, Math.min(radiusTL, maxRadius)); - float tr = Math.max(0.0f, Math.min(radiusTR, maxRadius)); - float br = Math.max(0.0f, Math.min(radiusBR, maxRadius)); - float bl = Math.max(0.0f, Math.min(radiusBL, maxRadius)); - - float scale = 1.0f; - float top = tl + tr; - if (top > w && top > 0.0f) { - scale = Math.min(scale, w / top); - } - float bottom = bl + br; - if (bottom > w && bottom > 0.0f) { - scale = Math.min(scale, w / bottom); - } - float left = tl + bl; - if (left > h && left > 0.0f) { - scale = Math.min(scale, h / left); - } - float right = tr + br; - if (right > h && right > 0.0f) { - scale = Math.min(scale, h / right); - } - - if (scale < 1.0f) { - tl *= scale; - tr *= scale; - br *= scale; - bl *= scale; - } - - out[0] = tl; - out[1] = tr; - out[2] = br; - out[3] = bl; - } - - public static void normalizeChamfers(double width, double height, - float chamferTL, float chamferTR, float chamferBR, float chamferBL, - float[] out) { - if (out == null || out.length < 4) { - throw new IllegalArgumentException("Output array must have length >= 4"); - } - - float w = (float) Math.abs(width); - float h = (float) Math.abs(height); - if (w <= 0.0f || h <= 0.0f) { - out[0] = 0.0f; - out[1] = 0.0f; - out[2] = 0.0f; - out[3] = 0.0f; - return; - } - - float max = Math.min(w, h); - float tl = Math.max(0.0f, Math.min(chamferTL, max)); - float tr = Math.max(0.0f, Math.min(chamferTR, max)); - float br = Math.max(0.0f, Math.min(chamferBR, max)); - float bl = Math.max(0.0f, Math.min(chamferBL, max)); - - float scale = 1.0f; - float top = tl + tr; - if (top > w && top > 0.0f) { - scale = Math.min(scale, w / top); - } - float bottom = bl + br; - if (bottom > w && bottom > 0.0f) { - scale = Math.min(scale, w / bottom); - } - float left = tl + bl; - if (left > h && left > 0.0f) { - scale = Math.min(scale, h / left); - } - float right = tr + br; - if (right > h && right > 0.0f) { - scale = Math.min(scale, h / right); - } - - if (scale < 1.0f) { - tl *= scale; - tr *= scale; - br *= scale; - bl *= scale; - } - - out[0] = tl; - out[1] = tr; - out[2] = br; - out[3] = bl; - } - - public static float normalizeDegrees(float angleDeg) { - float out = angleDeg % 360.0f; - return out < 0.0f ? out + 360.0f : out; - } - - public static void appendColoredQuad(MeshBuilder mesh, - double x, double y, double width, double height, - int cTopLeft, int cTopRight, int cBottomRight, int cBottomLeft) { - mesh.ensureQuadCapacity(); - - int tlA = (cTopLeft >>> 24) & 0xFF; - int tlR = (cTopLeft >>> 16) & 0xFF; - int tlG = (cTopLeft >>> 8) & 0xFF; - int tlB = cTopLeft & 0xFF; - int trA = (cTopRight >>> 24) & 0xFF; - int trR = (cTopRight >>> 16) & 0xFF; - int trG = (cTopRight >>> 8) & 0xFF; - int trB = cTopRight & 0xFF; - int brA = (cBottomRight >>> 24) & 0xFF; - int brR = (cBottomRight >>> 16) & 0xFF; - int brG = (cBottomRight >>> 8) & 0xFF; - int brB = cBottomRight & 0xFF; - int blA = (cBottomLeft >>> 24) & 0xFF; - int blR = (cBottomLeft >>> 16) & 0xFF; - int blG = (cBottomLeft >>> 8) & 0xFF; - int blB = cBottomLeft & 0xFF; - - int i1 = mesh.vec2(x, y).color(tlR, tlG, tlB, tlA).next(); - int i2 = mesh.vec2(x, y + height).color(blR, blG, blB, blA).next(); - int i3 = mesh.vec2(x + width, y + height).color(brR, brG, brB, brA).next(); - int i4 = mesh.vec2(x + width, y).color(trR, trG, trB, trA).next(); - mesh.quad(i1, i2, i3, i4); - } - - private static void emitUiPolygonTriangle(MeshBuilder mesh, int a, int b, int c, boolean reverseForUiCull) { - if (reverseForUiCull) { - mesh.triangle(a, c, b); - } else { - mesh.triangle(a, b, c); - } - } - - public static void appendPolygon(MeshBuilder mesh, double[] points, int pointCount, - int[] indices, int[] vertexIds, int argb) { - int safeCount = Math.min(pointCount, Math.min(points.length / 2, Math.min(indices.length, vertexIds.length))); - int triangles = safeCount - 2; - if (triangles <= 0) return; - mesh.ensureCapacity(safeCount, triangles * 3); - - int a = (argb >>> 24) & 0xFF; - int r = (argb >>> 16) & 0xFF; - int g = (argb >>> 8) & 0xFF; - int b = argb & 0xFF; - - for (int i = 0; i < safeCount; i++) { - indices[i] = i; - vertexIds[i] = mesh.vec2(points[i * 2], points[i * 2 + 1]).color(r, g, b, a).next(); - } - - double area = polygonArea(points, safeCount); - boolean reverseForUiCull = area >= 0.0; - if (isConvex(points, safeCount)) { - for (int i = 2; i < safeCount; i++) { - emitUiPolygonTriangle(mesh, vertexIds[0], vertexIds[i - 1], vertexIds[i], reverseForUiCull); - } - return; - } - - int remaining = safeCount; - double winding = area >= 0.0 ? 1.0 : -1.0; - int guard = safeCount * safeCount; - while (remaining > 2 && guard-- > 0) { - boolean clipped = false; - for (int i = 0; i < remaining; i++) { - int prev = i == 0 ? remaining - 1 : i - 1; - int next = i + 1 == remaining ? 0 : i + 1; - if (!isEar(points, indices, remaining, prev, i, next, winding)) continue; - emitUiPolygonTriangle(mesh, vertexIds[indices[prev]], vertexIds[indices[i]], vertexIds[indices[next]], reverseForUiCull); - for (int j = i; j < remaining - 1; j++) { - indices[j] = indices[j + 1]; - } - remaining--; - clipped = true; - break; - } - if (!clipped) { - for (int i = 2; i < remaining; i++) { - emitUiPolygonTriangle(mesh, vertexIds[indices[0]], vertexIds[indices[i - 1]], vertexIds[indices[i]], reverseForUiCull); - } - return; - } - } - } - - public static void appendPolygonGradient(MeshBuilder mesh, double[] points, int pointCount, - int[] indices, int[] vertexIds, - double x, double y, double width, double height, - int startArgb, int endArgb, float angleDeg, float offsetPx) { - int safeCount = Math.min(pointCount, Math.min(points.length / 2, Math.min(indices.length, vertexIds.length))); - int triangles = safeCount - 2; - if (triangles <= 0) return; - mesh.ensureCapacity(safeCount, triangles * 3); - - for (int i = 0; i < safeCount; i++) { - indices[i] = i; - int argb = linearGradientColorAt( - points[i * 2] - x, - points[i * 2 + 1] - y, - width, - height, - startArgb, - endArgb, - angleDeg, - offsetPx - ); - int a = (argb >>> 24) & 0xFF; - int r = (argb >>> 16) & 0xFF; - int g = (argb >>> 8) & 0xFF; - int b = argb & 0xFF; - vertexIds[i] = mesh.vec2(points[i * 2], points[i * 2 + 1]).color(r, g, b, a).next(); - } - - double area = polygonArea(points, safeCount); - boolean reverseForUiCull = area >= 0.0; - if (isConvex(points, safeCount)) { - for (int i = 2; i < safeCount; i++) { - emitUiPolygonTriangle(mesh, vertexIds[0], vertexIds[i - 1], vertexIds[i], reverseForUiCull); - } - return; - } - - int remaining = safeCount; - double winding = area >= 0.0 ? 1.0 : -1.0; - int guard = safeCount * safeCount; - while (remaining > 2 && guard-- > 0) { - boolean clipped = false; - for (int i = 0; i < remaining; i++) { - int prev = i == 0 ? remaining - 1 : i - 1; - int next = i + 1 == remaining ? 0 : i + 1; - if (!isEar(points, indices, remaining, prev, i, next, winding)) continue; - emitUiPolygonTriangle(mesh, vertexIds[indices[prev]], vertexIds[indices[i]], vertexIds[indices[next]], reverseForUiCull); - for (int j = i; j < remaining - 1; j++) { - indices[j] = indices[j + 1]; - } - remaining--; - clipped = true; - break; - } - if (!clipped) { - for (int i = 2; i < remaining; i++) { - emitUiPolygonTriangle(mesh, vertexIds[indices[0]], vertexIds[indices[i - 1]], vertexIds[indices[i]], reverseForUiCull); - } - return; - } - } - } - - public static void appendPolyline(MeshBuilder mesh, double[] points, int pointCount, - double thickness, boolean closed, int argb, boolean roundCapsAndJoins) { - int safeCount = Math.min(pointCount, points.length / 2); - int segments = closed ? safeCount : safeCount - 1; - if (segments <= 0) return; - mesh.ensureCapacity(segments * 4, segments * 6); - for (int i = 0; i < segments; i++) { - int next = i + 1; - if (next >= safeCount) next = 0; - appendThickSegment( - mesh, - points[i * 2], - points[i * 2 + 1], - points[next * 2], - points[next * 2 + 1], - thickness, - argb - ); - } - if (!roundCapsAndJoins) return; - double radius = thickness * 0.5; - if (closed) { - for (int i = 0; i < safeCount; i++) { - appendDisk(mesh, points[i * 2], points[i * 2 + 1], radius, 10, argb); - } - return; - } - appendDisk(mesh, points[0], points[1], radius, 10, argb); - for (int i = 1; i < safeCount - 1; i++) { - appendDisk(mesh, points[i * 2], points[i * 2 + 1], radius, 10, argb); - } - appendDisk(mesh, points[(safeCount - 1) * 2], points[(safeCount - 1) * 2 + 1], radius, 10, argb); - } - - public static void appendPolylineGradient(MeshBuilder mesh, double[] points, int pointCount, - double thickness, boolean closed, - int startArgb, int endArgb, - boolean roundCapsAndJoins) { - int safeCount = Math.min(pointCount, points.length / 2); - int segments = closed ? safeCount : safeCount - 1; - if (segments <= 0) return; - mesh.ensureCapacity(segments * 4, segments * 6); - for (int i = 0; i < segments; i++) { - int next = i + 1; - if (next >= safeCount) next = 0; - float t0 = segments <= 1 ? 0.0f : (float) i / (float) segments; - float t1 = segments <= 1 ? 1.0f : (float) (i + 1) / (float) segments; - appendThickSegmentGradient( - mesh, - points[i * 2], - points[i * 2 + 1], - points[next * 2], - points[next * 2 + 1], - thickness, - mixArgb(startArgb, endArgb, clamp01(t0)), - mixArgb(startArgb, endArgb, clamp01(t1)) - ); - } - if (!roundCapsAndJoins) return; - double radius = thickness * 0.5; - for (int i = 0; i < safeCount; i++) { - float t = safeCount <= 1 ? 0.0f : (float) i / (float) (safeCount - 1); - appendDisk(mesh, points[i * 2], points[i * 2 + 1], radius, 10, mixArgb(startArgb, endArgb, clamp01(t))); - } - } - - public static void appendPolylineLinearGradient(MeshBuilder mesh, double[] points, int pointCount, - double thickness, boolean closed, - double x, double y, double width, double height, - int startArgb, int endArgb, float angleDeg, float offsetPx, - boolean roundCapsAndJoins) { - int safeCount = Math.min(pointCount, points.length / 2); - int segments = closed ? safeCount : safeCount - 1; - if (segments <= 0) return; - mesh.ensureCapacity(segments * 4, segments * 6); - for (int i = 0; i < segments; i++) { - int next = i + 1; - if (next >= safeCount) next = 0; - int c0 = linearGradientColorAt(points[i * 2] - x, points[i * 2 + 1] - y, - width, height, startArgb, endArgb, angleDeg, offsetPx); - int c1 = linearGradientColorAt(points[next * 2] - x, points[next * 2 + 1] - y, - width, height, startArgb, endArgb, angleDeg, offsetPx); - appendThickSegmentGradient( - mesh, - points[i * 2], - points[i * 2 + 1], - points[next * 2], - points[next * 2 + 1], - thickness, - c0, - c1 - ); - } - if (!roundCapsAndJoins) return; - double radius = thickness * 0.5; - for (int i = 0; i < safeCount; i++) { - int c = linearGradientColorAt(points[i * 2] - x, points[i * 2 + 1] - y, - width, height, startArgb, endArgb, angleDeg, offsetPx); - appendDisk(mesh, points[i * 2], points[i * 2 + 1], radius, 10, c); - } - } - - private static void appendThickSegment(MeshBuilder mesh, - double x1, - double y1, - double x2, - double y2, - double thickness, - int argb) { - double dx = x2 - x1; - double dy = y2 - y1; - double len = Math.sqrt(dx * dx + dy * dy); - if (len <= 0.0001) return; - double half = thickness * 0.5; - double nx = -dy / len * half; - double ny = dx / len * half; - - int a = (argb >>> 24) & 0xFF; - int r = (argb >>> 16) & 0xFF; - int g = (argb >>> 8) & 0xFF; - int b = argb & 0xFF; - - int i1 = mesh.vec2(x1 - nx, y1 - ny).color(r, g, b, a).next(); - int i2 = mesh.vec2(x1 + nx, y1 + ny).color(r, g, b, a).next(); - int i3 = mesh.vec2(x2 + nx, y2 + ny).color(r, g, b, a).next(); - int i4 = mesh.vec2(x2 - nx, y2 - ny).color(r, g, b, a).next(); - mesh.quad(i1, i2, i3, i4); - } - - private static void appendThickSegmentGradient(MeshBuilder mesh, - double x1, - double y1, - double x2, - double y2, - double thickness, - int startArgb, - int endArgb) { - double dx = x2 - x1; - double dy = y2 - y1; - double len = Math.sqrt(dx * dx + dy * dy); - if (len <= 0.0001) return; - double half = thickness * 0.5; - double nx = -dy / len * half; - double ny = dx / len * half; - - int sa = (startArgb >>> 24) & 0xFF; - int sr = (startArgb >>> 16) & 0xFF; - int sg = (startArgb >>> 8) & 0xFF; - int sb = startArgb & 0xFF; - int ea = (endArgb >>> 24) & 0xFF; - int er = (endArgb >>> 16) & 0xFF; - int eg = (endArgb >>> 8) & 0xFF; - int eb = endArgb & 0xFF; - - int i1 = mesh.vec2(x1 - nx, y1 - ny).color(sr, sg, sb, sa).next(); - int i2 = mesh.vec2(x1 + nx, y1 + ny).color(sr, sg, sb, sa).next(); - int i3 = mesh.vec2(x2 + nx, y2 + ny).color(er, eg, eb, ea).next(); - int i4 = mesh.vec2(x2 - nx, y2 - ny).color(er, eg, eb, ea).next(); - mesh.quad(i1, i2, i3, i4); - } - - private static void appendDisk(MeshBuilder mesh, - double cx, - double cy, - double radius, - int segments, - int argb) { - if (radius <= 0.0) return; - int safeSegments = Math.max(6, Math.min(24, segments)); - mesh.ensureCapacity(safeSegments + 1, safeSegments * 3); - - int a = (argb >>> 24) & 0xFF; - int r = (argb >>> 16) & 0xFF; - int g = (argb >>> 8) & 0xFF; - int b = argb & 0xFF; - - int center = mesh.vec2(cx, cy).color(r, g, b, a).next(); - int first = -1; - int prev = -1; - for (int i = 0; i < safeSegments; i++) { - double angle = Math.PI * 2.0 * i / safeSegments; - int vertex = mesh.vec2(cx + Math.cos(angle) * radius, cy + Math.sin(angle) * radius) - .color(r, g, b, a) - .next(); - if (first < 0) { - first = vertex; - } else { - mesh.triangle(center, prev, vertex); - } - prev = vertex; - } - if (first >= 0 && prev >= 0) { - mesh.triangle(center, prev, first); - } - } - - public static int buildRoundedProgressRect(double[] out, - double[] work, - double x, - double y, - double width, - double height, - double radius, - double clipX, - boolean fromRight) { - int sourceCount = buildRoundedRectPolygon(work, x, y, width, height, radius); - if (sourceCount < 3) return 0; - return clipPolygonVertical(work, sourceCount, out, clipX, fromRight); - } - - public static int buildRoundedRectPolygon(double[] out, - double x, - double y, - double width, - double height, - double radius) { - double w = Math.max(0.0, width); - double h = Math.max(0.0, height); - if (w <= 0.0 || h <= 0.0) return 0; - double x2 = x + w; - double y2 = y + h; - double r = Math.max(0.0, Math.min(Math.min(w, h) * 0.5, radius)); - int[] count = {0}; - if (r <= 0.001) { - add(out, out.length / 2, count, x, y); - add(out, out.length / 2, count, x2, y); - add(out, out.length / 2, count, x2, y2); - add(out, out.length / 2, count, x, y2); - return count[0]; - } - - int segments = Math.max(4, Math.min(10, (int) Math.ceil(r * 1.35))); - add(out, out.length / 2, count, x + r, y); - add(out, out.length / 2, count, x2 - r, y); - addArc(out, out.length / 2, count, x2 - r, y + r, r, -90.0, 0.0, segments); - add(out, out.length / 2, count, x2, y2 - r); - addArc(out, out.length / 2, count, x2 - r, y2 - r, r, 0.0, 90.0, segments); - add(out, out.length / 2, count, x + r, y2); - addArc(out, out.length / 2, count, x + r, y2 - r, r, 90.0, 180.0, segments); - add(out, out.length / 2, count, x, y + r); - addArc(out, out.length / 2, count, x + r, y + r, r, 180.0, 270.0, segments); - return removeClosingDuplicate(out, count[0]); - } - - private static int removeClosingDuplicate(double[] points, int count) { - if (points == null || count < 2) return count; - double firstX = points[0]; - double firstY = points[1]; - double lastX = points[(count - 1) * 2]; - double lastY = points[(count - 1) * 2 + 1]; - if (Math.abs(firstX - lastX) < 0.0001 && Math.abs(firstY - lastY) < 0.0001) { - return count - 1; - } - return count; - } - - private static void add(double[] out, int max, int[] count, double x, double y) { - if (out == null || count == null || count.length == 0) return; - int index = count[0]; - if (index < 0 || index >= max) return; - int base = index * 2; - if (base + 1 >= out.length) return; - out[base] = x; - out[base + 1] = y; - count[0] = index + 1; - } - - private static void addArc(double[] out, - int max, - int[] count, - double cx, - double cy, - double radius, - double startDeg, - double endDeg, - int segments) { - int safeSegments = Math.max(1, segments); - for (int i = 1; i <= safeSegments; i++) { - double t = i / (double) safeSegments; - double a = Math.toRadians(startDeg + (endDeg - startDeg) * t); - add(out, max, count, cx + Math.cos(a) * radius, cy + Math.sin(a) * radius); - } - } - - private static int clipPolygonVertical(double[] source, - int sourceCount, - double[] out, - double clipX, - boolean keepRight) { - int maxOut = out.length / 2; - int[] count = {0}; - if (source == null || out == null || sourceCount < 3 || maxOut <= 0) return 0; - - for (int i = 0; i < sourceCount; i++) { - int prev = i == 0 ? sourceCount - 1 : i - 1; - double sx = source[prev * 2]; - double sy = source[prev * 2 + 1]; - double ex = source[i * 2]; - double ey = source[i * 2 + 1]; - boolean sInside = keepRight ? sx >= clipX - 0.0001 : sx <= clipX + 0.0001; - boolean eInside = keepRight ? ex >= clipX - 0.0001 : ex <= clipX + 0.0001; - - if (sInside && eInside) { - add(out, maxOut, count, ex, ey); - } else if (sInside) { - addVerticalIntersection(out, maxOut, count, sx, sy, ex, ey, clipX); - } else if (eInside) { - addVerticalIntersection(out, maxOut, count, sx, sy, ex, ey, clipX); - add(out, maxOut, count, ex, ey); - } - } - return count[0]; - } - - private static void addVerticalIntersection(double[] out, - int max, - int[] count, - double sx, - double sy, - double ex, - double ey, - double clipX) { - double dx = ex - sx; - if (Math.abs(dx) <= 0.000001) { - add(out, max, count, clipX, ey); - return; - } - double t = (clipX - sx) / dx; - t = Math.max(0.0, Math.min(1.0, t)); - add(out, max, count, clipX, sy + (ey - sy) * t); - } - - public static void roundedRectAnchor(double[] out, - int offset, - double x, - double y, - double width, - double height, - double radius, - double targetX, - double targetY) { - double cx = x + width * 0.5; - double cy = y + height * 0.5; - double halfW = Math.abs(width) * 0.5; - double halfH = Math.abs(height) * 0.5; - if (halfW <= 0.0 || halfH <= 0.0) { - out[offset] = cx; - out[offset + 1] = cy; - return; - } - - double dx = targetX - cx; - double dy = targetY - cy; - if (Math.abs(dx) <= 0.0001 && Math.abs(dy) <= 0.0001) { - out[offset] = cx; - out[offset + 1] = cy; - return; - } - - double r = Math.max(0.0, Math.min(Math.min(halfW, halfH), radius)); - double innerW = halfW - r; - double innerH = halfH - r; - double bestT = Double.POSITIVE_INFINITY; - - if (Math.abs(dx) > 0.0001) { - double tx = (dx > 0.0 ? halfW : -halfW) / dx; - double yy = dy * tx; - if (tx > 0.0 && Math.abs(yy) <= innerH + 0.0001) { - bestT = Math.min(bestT, tx); - } - } - if (Math.abs(dy) > 0.0001) { - double ty = (dy > 0.0 ? halfH : -halfH) / dy; - double xx = dx * ty; - if (ty > 0.0 && Math.abs(xx) <= innerW + 0.0001) { - bestT = Math.min(bestT, ty); - } - } - if (r > 0.0) { - double circleX = dx >= 0.0 ? innerW : -innerW; - double circleY = dy >= 0.0 ? innerH : -innerH; - double a = dx * dx + dy * dy; - double b = -2.0 * (dx * circleX + dy * circleY); - double c = circleX * circleX + circleY * circleY - r * r; - double discriminant = b * b - 4.0 * a * c; - if (a > 0.0001 && discriminant >= 0.0) { - double sqrt = Math.sqrt(discriminant); - double t0 = (-b - sqrt) / (2.0 * a); - double t1 = (-b + sqrt) / (2.0 * a); - if (t0 > 0.0) bestT = Math.min(bestT, t0); - if (t1 > 0.0) bestT = Math.min(bestT, t1); - } - } - if (!Double.isFinite(bestT)) { - double tx = Math.abs(dx) > 0.0001 ? halfW / Math.abs(dx) : Double.POSITIVE_INFINITY; - double ty = Math.abs(dy) > 0.0001 ? halfH / Math.abs(dy) : Double.POSITIVE_INFINITY; - bestT = Math.min(tx, ty); - } - - out[offset] = cx + dx * bestT; - out[offset + 1] = cy + dy * bestT; - } - - private static int buildChamferedRect(double[] out, - double x, - double y, - double width, - double height, - double chamfer) { - double c = Math.max(0.0, Math.min(Math.min(Math.abs(width), Math.abs(height)) * 0.5, chamfer)); - double x2 = x + width; - double y2 = y + height; - out[0] = x + c; - out[1] = y; - out[2] = x2 - c; - out[3] = y; - out[4] = x2; - out[5] = y + c; - out[6] = x2; - out[7] = y2 - c; - out[8] = x2 - c; - out[9] = y2; - out[10] = x + c; - out[11] = y2; - out[12] = x; - out[13] = y2 - c; - out[14] = x; - out[15] = y + c; - return 8; - } - - public static int buildNotchedRect(double[] out, - double x, - double y, - double width, - double height, - double notchWidth, - double notchDepth) { - double nw = Math.max(0.0, Math.min(Math.abs(width) * 0.5, notchWidth)); - double nd = Math.max(0.0, Math.min(Math.abs(height) * 0.5, notchDepth)); - double x2 = x + width; - double y2 = y + height; - double cx = x + width * 0.5; - out[0] = x; - out[1] = y; - out[2] = cx - nw; - out[3] = y; - out[4] = cx - nw; - out[5] = y + nd; - out[6] = cx + nw; - out[7] = y + nd; - out[8] = cx + nw; - out[9] = y; - out[10] = x2; - out[11] = y; - out[12] = x2; - out[13] = y2; - out[14] = x; - out[15] = y2; - return 8; - } - - public static int buildBezier(double[] out, - double x1, - double y1, - double cx1, - double cy1, - double cx2, - double cy2, - double x2, - double y2, - int segments) { - int count = Math.max(2, Math.min(32, segments + 1)); - for (int i = 0; i < count; i++) { - double t = (double) i / (count - 1); - double u = 1.0 - t; - double x = u * u * u * x1 - + 3.0 * u * u * t * cx1 - + 3.0 * u * t * t * cx2 - + t * t * t * x2; - double y = u * u * u * y1 - + 3.0 * u * u * t * cy1 - + 3.0 * u * t * t * cy2 - + t * t * t * y2; - out[i * 2] = x; - out[i * 2 + 1] = y; - } - return count; - } - - private static boolean isConvex(double[] points, int count) { - double sign = 0.0; - for (int i = 0; i < count; i++) { - int i0 = i; - int i1 = (i + 1) % count; - int i2 = (i + 2) % count; - double cross = cross(points, i0, i1, i2); - if (Math.abs(cross) <= 0.0001) continue; - if (sign == 0.0) { - sign = Math.signum(cross); - } else if (Math.signum(cross) != sign) { - return false; - } - } - return true; - } - - private static boolean isEar(double[] points, int[] vertexIds, int remaining, int prev, int i, int next, double winding) { - int p = vertexIds[prev]; - int c = vertexIds[i]; - int n = vertexIds[next]; - if (crossByVertex(points, p, c, n) * winding <= 0.0001) return false; - double ax = points[(p) * 2]; - double ay = points[(p) * 2 + 1]; - double bx = points[(c) * 2]; - double by = points[(c) * 2 + 1]; - double cx = points[(n) * 2]; - double cy = points[(n) * 2 + 1]; - for (int j = 0; j < remaining; j++) { - if (j == prev || j == i || j == next) continue; - int v = vertexIds[j]; - if (pointInTriangle(points[v * 2], points[v * 2 + 1], ax, ay, bx, by, cx, cy, winding)) { - return false; - } - } - return true; - } - - private static double cross(double[] points, int i0, int i1, int i2) { - double ax = points[i0 * 2]; - double ay = points[i0 * 2 + 1]; - double bx = points[i1 * 2]; - double by = points[i1 * 2 + 1]; - double cx = points[i2 * 2]; - double cy = points[i2 * 2 + 1]; - return (bx - ax) * (cy - ay) - (by - ay) * (cx - ax); - } - - private static double crossByVertex(double[] points, int i0, int i1, int i2) { - return cross(points, i0, i1, i2); - } - - private static double polygonArea(double[] points, int count) { - double area = 0.0; - for (int i = 0; i < count; i++) { - int j = i + 1 == count ? 0 : i + 1; - area += points[i * 2] * points[j * 2 + 1] - points[j * 2] * points[i * 2 + 1]; - } - return area * 0.5; - } - - private static boolean pointInTriangle(double px, double py, - double ax, double ay, - double bx, double by, - double cx, double cy, - double winding) { - double c1 = (bx - ax) * (py - ay) - (by - ay) * (px - ax); - double c2 = (cx - bx) * (py - by) - (cy - by) * (px - bx); - double c3 = (ax - cx) * (py - cy) - (ay - cy) * (px - cx); - return c1 * winding >= 0.0 && c2 * winding >= 0.0 && c3 * winding >= 0.0; - } - - public static int mixArgb(int start, int end, float t) { - int sa = (start >>> 24) & 0xFF; - int sr = (start >>> 16) & 0xFF; - int sg = (start >>> 8) & 0xFF; - int sb = start & 0xFF; - - int ea = (end >>> 24) & 0xFF; - int er = (end >>> 16) & 0xFF; - int eg = (end >>> 8) & 0xFF; - int eb = end & 0xFF; - - int a = (int) (sa + (ea - sa) * t + 0.5f); - int r = (int) (sr + (er - sr) * t + 0.5f); - int g = (int) (sg + (eg - sg) * t + 0.5f); - int b = (int) (sb + (eb - sb) * t + 0.5f); - - return (a << 24) | (r << 16) | (g << 8) | b; - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/MeshBuilder.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/MeshBuilder.java deleted file mode 100644 index 5a6c8c5..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/MeshBuilder.java +++ /dev/null @@ -1,738 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.render.engine.uniform; - -import com.mojang.blaze3d.buffers.GpuBuffer; -import com.mojang.blaze3d.pipeline.RenderPipeline; -import com.mojang.blaze3d.systems.RenderSystem; -import com.mojang.blaze3d.vertex.VertexFormat; -import net.fabricmc.loader.api.FabricLoader; -import net.minecraft.client.Minecraft; -import net.minecraft.world.phys.Vec3; -import dev.loki.lovisual.render.engine.RenderState; -import dev.loki.lovisual.render.engine.color.RenderColor; -import dev.loki.lovisual.render.engine.guard.LegacyRenderPath; -import dev.loki.lovisual.render.engine.guard.RenderArchitectureGuard; -import dev.loki.lovisual.render.engine.renderer.warp.RenderWarp; -import dev.loki.lovisual.render.engine.renderer.warp.RenderWarpStack; - -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.util.Objects; - -import static org.lwjgl.system.MemoryUtil.*; - -/** - * Reusable CPU-side mesh builder backed by native direct buffers. - * - *

Geometry is written between {@link #begin()} and {@link #end()}, then consumed through - * {@link #vertexBufferView()} and {@link #indexBufferView()} by the RHI dynamic-mesh upload path. - * The builder owns only its CPU buffers; GPU buffer allocation and lifetime are managed by the renderer.

- * - *

Reserve or ensure enough capacity before writing each primitive. Call {@link #close()} when the - * builder itself is no longer needed to release its native memory.

- */ -public final class MeshBuilder implements AutoCloseable { - private static final boolean DEBUG = - FabricLoader.getInstance().isDevelopmentEnvironment() || Boolean.getBoolean("lovisual.render.debug"); - private static final int DEFAULT_VERTEX_CAPACITY = Integer.getInteger("lovisual.render.mesh.defaultVertices", 4096); - private static final int DEFAULT_INDEX_CAPACITY = Integer.getInteger("lovisual.render.mesh.defaultIndices", 8192); - private static final int MAX_BUFFER_BYTES = Integer.MAX_VALUE - 8; - private static final FrameStats FRAME_STATS = new FrameStats(); - private final VertexFormat format; - private final int vertexStride; - private final int primitiveIndicesCount; - private final double[] warped2 = new double[2]; - public double alpha = 1.0; - private ByteBuffer vertices; - private long verticesPtrStart, verticesPtr; - private ByteBuffer indices; - private long indicesPtr; - private int vertexI; - private int indicesCount; - private boolean building; - private boolean worldCameraAnchored; - private double cameraX, cameraZ; - - public MeshBuilder(RenderPipeline pipeline) { - this(Objects.requireNonNull(pipeline.getVertexFormatBinding(0)), pipeline.getPrimitiveTopology()); - } - - public MeshBuilder(VertexFormat format, com.mojang.blaze3d.PrimitiveTopology drawMode) { - this.format = format; - this.vertexStride = format.getVertexSize(); - this.primitiveIndicesCount = drawMode.primitiveLength; - } - - public MeshBuilder(VertexFormat format, com.mojang.blaze3d.PrimitiveTopology drawMode, int vertexCount, int indexCount) { - this(format, drawMode); - reserve(vertexCount, indexCount); - } - - private static ByteBuffer allocateDirect(int bytes) { - if (bytes <= 0) { - throw new IllegalArgumentException("MeshBuilder buffer size must be positive: " + bytes); - } - if (bytes > MAX_BUFFER_BYTES) { - throw new OutOfMemoryError("MeshBuilder buffer request is too large: " + bytes); - } - return memAlloc(bytes); - } - - private static void freeDirect(ByteBuffer buffer) { - if (buffer != null) { - memFree(buffer); - } - } - - public static void resetFrameStats() { - FRAME_STATS.reset(); - } - - public static FrameStatsSnapshot frameStatsSnapshot() { - return FRAME_STATS.snapshot(); - } - - public void begin() { - if (RenderState.rendering3D) { - beginWorld(resolveRenderCameraPos()); - } else { - beginScreen(); - } - } - - /** - * Begin a 2D/screen-space mesh. No world camera anchoring is applied. - */ - public void beginScreen() { - beginInternal(false, Vec3.ZERO); - } - - /** - * Begin a world-space mesh with an explicit camera anchor. - * - *

World vertices keep full Y, but X/Z are written camera-relative to avoid - * float precision loss. The RHI world pass then applies the matching -cameraY - * translation immediately before writing the MeshData UBO. Do not make this - * depend on ambient RenderState in world command paths: the caller must pass - * the camera that belongs to the current frame.

- */ - public void beginWorld(Vec3 cameraPos) { - if (cameraPos == null) { - throw new IllegalArgumentException("MeshBuilder.beginWorld() requires a camera position"); - } - beginInternal(true, cameraPos); - } - - private void beginInternal(boolean worldCameraAnchored, Vec3 cameraPos) { - if (building) throw new IllegalStateException("Mesh.begin() called while already building."); - - if (vertices == null || indices == null) reserve(DEFAULT_VERTEX_CAPACITY, DEFAULT_INDEX_CAPACITY); - - verticesPtr = verticesPtrStart; - vertexI = 0; - indicesCount = 0; - - this.worldCameraAnchored = worldCameraAnchored; - this.cameraX = worldCameraAnchored ? cameraPos.x : 0.0; - this.cameraZ = worldCameraAnchored ? cameraPos.z : 0.0; - - building = true; - } - - private static Vec3 resolveRenderCameraPos() { - if (RenderState.cameraPos != null && RenderState.cameraPos != Vec3.ZERO) { - return RenderState.cameraPos; - } - Minecraft mc = Minecraft.getInstance(); - if (mc != null && mc.gameRenderer != null && mc.gameRenderer.mainCamera() != null) { - return mc.gameRenderer.mainCamera().position(); - } - return Vec3.ZERO; - } - - public MeshBuilder vec3(double x, double y, double z) { - debugVertexWriteCapacity(12, "Position3"); - - long p = verticesPtr; - - memPutFloat(p, (float) (x - cameraX)); - memPutFloat(p + 4, (float) y); - memPutFloat(p + 8, (float) (z - cameraZ)); - - verticesPtr += 12; - return this; - } - - public MeshBuilder vec2(double x, double y) { - debugVertexWriteCapacity(8, "Position2"); - - RenderWarp warp = RenderWarpStack.current(); - if (warp.active()) { - warp.map(x, y, warped2); - x = warped2[0]; - y = warped2[1]; - } - - long p = verticesPtr; - - memPutFloat(p, (float) x); - memPutFloat(p + 4, (float) y); - - verticesPtr += 8; - return this; - } - - public MeshBuilder raw2(double x, double y) { - debugVertexWriteCapacity(8, "Raw2"); - - long p = verticesPtr; - - memPutFloat(p, (float) x); - memPutFloat(p + 4, (float) y); - - verticesPtr += 8; - return this; - } - - public MeshBuilder local2(double x, double y) { - debugVertexWriteCapacity(16, "Local"); - - RenderWarp warp = RenderWarpStack.current(); - double invW = warp.active() ? warp.interpolationInvW(x, y) : 1.0; - - long p = verticesPtr; - - memPutFloat(p, (float) (x * invW)); - memPutFloat(p + 4, (float) (y * invW)); - memPutFloat(p + 8, (float) invW); - memPutFloat(p + 12, 0.0f); - - verticesPtr += 16; - return this; - } - - public MeshBuilder vec4(float x, float y, float z, float w) { - debugVertexWriteCapacity(16, "Vec4"); - - long p = verticesPtr; - - memPutFloat(p, x); - memPutFloat(p + 4, y); - memPutFloat(p + 8, z); - memPutFloat(p + 12, w); - - verticesPtr += 16; - return this; - } - - public MeshBuilder vec4(double x, double y, double z, double w) { - return vec4((float) x, (float) y, (float) z, (float) w); - } - - /** - * Color as bytes 0..255 - */ - public MeshBuilder color(int r, int g, int b, int a) { - debugVertexWriteCapacity(4, "Color"); - - long p = verticesPtr; - - memPutByte(p, (byte) r); - memPutByte(p + 1, (byte) g); - memPutByte(p + 2, (byte) b); - memPutByte(p + 3, (byte) Math.max(0, Math.min(255, (int) (a * alpha)))); - - verticesPtr += 4; - return this; - } - - /** - * Color as floats 0..1 - */ - public MeshBuilder color(float r, float g, float b, float a) { - return color( - (int) (r * 255f), - (int) (g * 255f), - (int) (b * 255f), - (int) (a * 255f) - ); - } - - /** - * Color from RenderColor (0..255). - */ - public MeshBuilder color(RenderColor color) { - return color(color.r, color.g, color.b, color.a); - } - - public int next() { - debugCompletedVertex(); - return vertexI++; - } - - public void line(int i1, int i2) { - debugIndexWriteCapacity(2, "line"); - - long p = indicesPtr + indicesCount * 4L; - - memPutInt(p, i1); - memPutInt(p + 4, i2); - - indicesCount += 2; - } - - public void triangle(int i1, int i2, int i3) { - debugIndexWriteCapacity(3, "triangle"); - - long p = indicesPtr + indicesCount * 4L; - - memPutInt(p, i1); - memPutInt(p + 4, i2); - memPutInt(p + 8, i3); - - indicesCount += 3; - } - - public void quad(int i1, int i2, int i3, int i4) { - debugIndexWriteCapacity(6, "quad"); - - long p = indicesPtr + indicesCount * 4L; - - memPutInt(p, i1); - memPutInt(p + 4, i2); - memPutInt(p + 8, i3); - - memPutInt(p + 12, i3); - memPutInt(p + 16, i4); - memPutInt(p + 20, i1); - - indicesCount += 6; - } - - public void ensureLineCapacity() { - ensureCapacity(2, 2); - } - - public void ensureTriCapacity() { - ensureCapacity(3, 3); - } - - public void ensureQuadCapacity() { - ensureCapacity(4, 6); - } - - public void ensureCapacity(int addVertices, int addIndices) { - if (DEBUG && (addIndices % primitiveIndicesCount != 0)) { - throw new IllegalArgumentException("Unexpected amount of indices written to MeshBuilder."); - } - - if (vertices == null || indices == null) { - reserve(Math.max(DEFAULT_VERTEX_CAPACITY, vertexI + addVertices), - Math.max(DEFAULT_INDEX_CAPACITY, indicesCount + addIndices)); - return; - } - - int vNeedBytes = (vertexI + addVertices) * vertexStride; - if (vNeedBytes > vertices.capacity()) { - int offset = getVerticesOffset(); - int newSize = Math.max(vertices.capacity() * 2, vertices.capacity() + addVertices * vertexStride); - - ByteBuffer oldVertices = vertices; - ByteBuffer newVertices = allocateDirect(newSize); - memCopy(memAddress0(oldVertices), memAddress0(newVertices), offset); - - FRAME_STATS.vertexReallocs++; - FRAME_STATS.allocatedBytes += newSize; - FRAME_STATS.maxVertexBytes = Math.max(FRAME_STATS.maxVertexBytes, newSize); - - vertices = newVertices; - verticesPtrStart = memAddress0(vertices); - verticesPtr = verticesPtrStart + offset; - freeDirect(oldVertices); - } - - int iNeedBytes = (indicesCount + addIndices) * Integer.BYTES; - if (iNeedBytes > indices.capacity()) { - int newSize = Math.max(indices.capacity() * 2, indices.capacity() + addIndices * Integer.BYTES); - - ByteBuffer oldIndices = indices; - ByteBuffer newIndices = allocateDirect(newSize); - memCopy(memAddress0(oldIndices), memAddress0(newIndices), (long) indicesCount * 4L); - - FRAME_STATS.indexReallocs++; - FRAME_STATS.allocatedBytes += newSize; - FRAME_STATS.maxIndexBytes = Math.max(FRAME_STATS.maxIndexBytes, newSize); - - indices = newIndices; - indicesPtr = memAddress0(indices); - freeDirect(oldIndices); - } - } - - /** - * Ensures that the CPU-side direct buffers can hold at least the requested amount of vertices/indices. - * - *

This method is intentionally separate from {@link #begin()}: render code that owns a long-lived - * MeshBuilder can pre-warm it during initialization instead of paying ByteBuffer.allocateDirect on the - * render-thread hot path. If the current buffers are already large enough, this is a no-op.

- */ - public void reserve(int vertexCount, int indexCount) { - int vertexBytes = vertexStride * Math.max(1, vertexCount); - int indexBytes = Math.max(1, indexCount) * Integer.BYTES; - boolean initialAllocation = vertices == null && indices == null; - - if (vertices == null || vertexBytes > vertices.capacity()) { - int offset = vertices != null ? getVerticesOffset() : 0; - ByteBuffer oldVertices = vertices; - ByteBuffer newVertices = allocateDirect(vertexBytes); - if (oldVertices != null && offset > 0) { - memCopy(memAddress0(oldVertices), memAddress0(newVertices), offset); - } - - vertices = newVertices; - verticesPtrStart = memAddress0(vertices); - verticesPtr = verticesPtrStart + offset; - freeDirect(oldVertices); - - if (!initialAllocation) { - FRAME_STATS.vertexReallocs++; - } - FRAME_STATS.allocatedBytes += vertexBytes; - FRAME_STATS.maxVertexBytes = Math.max(FRAME_STATS.maxVertexBytes, vertexBytes); - } - - if (indices == null || indexBytes > indices.capacity()) { - long offset = indices != null ? (long) indicesCount * Integer.BYTES : 0L; - ByteBuffer oldIndices = indices; - ByteBuffer newIndices = allocateDirect(indexBytes); - if (oldIndices != null && offset > 0L) { - memCopy(memAddress0(oldIndices), memAddress0(newIndices), offset); - } - - indices = newIndices; - indicesPtr = memAddress0(indices); - freeDirect(oldIndices); - - if (!initialAllocation) { - FRAME_STATS.indexReallocs++; - } - FRAME_STATS.allocatedBytes += indexBytes; - FRAME_STATS.maxIndexBytes = Math.max(FRAME_STATS.maxIndexBytes, indexBytes); - } - - if (initialAllocation && vertices != null && indices != null) { - FRAME_STATS.initialAllocations++; - } - } - - public void end() { - validateComplete("MeshBuilder.end"); - building = false; - } - - public boolean isBuilding() { - return building; - } - - /** - * Append an already-built mesh into this builder, rebasing INT indices by the current vertex count. - * This is used by higher level batchers (UI/text) to preserve draw order without flushing per element. - */ - public void appendMesh(MeshBuilder source) { - if (source == null || source.getVertexCount() <= 0 || source.getIndicesCount() <= 0) return; - if (source.isBuilding()) source.end(); - if (!building) { - throw new IllegalStateException("appendMesh() requires destination mesh to be building."); - } - if (this.vertexStride != source.vertexStride || !this.format.equals(source.format)) { - throw new IllegalArgumentException("Cannot append mesh with different vertex format/stride."); - } - - int sourceVertices = source.getVertexCount(); - int sourceIndices = source.getIndicesCount(); - int baseVertex = this.vertexI; - ensureCapacity(sourceVertices, sourceIndices); - - ByteBuffer vertexSource = source.vertexBufferView(); - long vertexBytes = source.getVertexBytes(); - memCopy(memAddress0(vertexSource), verticesPtr, vertexBytes); - verticesPtr += vertexBytes; - vertexI += sourceVertices; - - ByteBuffer indexSource = source.indexBufferView(); - long indexIn = memAddress0(indexSource); - long indexOut = indicesPtr + (long) indicesCount * Integer.BYTES; - for (int i = 0; i < sourceIndices; i++) { - memPutInt(indexOut + (long) i * Integer.BYTES, memGetInt(indexIn + (long) i * Integer.BYTES) + baseVertex); - } - indicesCount += sourceIndices; - } - - public ByteBuffer vertexBufferView() { - validateComplete("MeshBuilder.vertexBufferView"); - ByteBuffer copy = vertices.duplicate().order(ByteOrder.nativeOrder()); - copy.position(0); - copy.limit(getVerticesOffset()); - return copy.slice().order(ByteOrder.nativeOrder()); - } - - public ByteBuffer indexBufferView() { - validateComplete("MeshBuilder.indexBufferView"); - ByteBuffer copy = indices.duplicate().order(ByteOrder.nativeOrder()); - copy.position(0); - copy.limit(indicesCount * Integer.BYTES); - return copy.slice().order(ByteOrder.nativeOrder()); - } - - public int getVertexBytes() { - return getVerticesOffset(); - } - - public int getVertexStride() { - return vertexStride; - } - - public boolean isWorldCameraAnchored() { - return worldCameraAnchored; - } - - public double cameraAnchorX() { - return cameraX; - } - - public double cameraAnchorZ() { - return cameraZ; - } - - public int getIndexBytes() { - return indicesCount * Integer.BYTES; - } - - /** - * Emergency compatibility upload. This path is locked after the RHI migration and is not allowed as production - * behavior. Use LoVisualRHI.dynamicMeshes().upload(mesh) instead. - */ - @Deprecated - public GpuBuffer getVertexBuffer() { - RenderArchitectureGuard.requireAllowed(LegacyRenderPath.IMMEDIATE_MESH_UPLOAD, "MeshBuilder#getVertexBuffer"); - return RenderSystem.getDevice().createBuffer( - () -> "lovisual-immediate-vertices", - GpuBuffer.USAGE_VERTEX | GpuBuffer.USAGE_COPY_DST, - vertexBufferView() - ); - } - - /** - * Emergency compatibility upload. This path is locked after the RHI migration and is not allowed as production - * behavior. Use LoVisualRHI.dynamicMeshes().upload(mesh) instead. - */ - @Deprecated - public GpuBuffer getIndexBuffer() { - RenderArchitectureGuard.requireAllowed(LegacyRenderPath.IMMEDIATE_MESH_UPLOAD, "MeshBuilder#getIndexBuffer"); - return RenderSystem.getDevice().createBuffer( - () -> "lovisual-immediate-indices", - GpuBuffer.USAGE_INDEX | GpuBuffer.USAGE_COPY_DST, - indexBufferView() - ); - } - - public com.mojang.blaze3d.IndexType getIndexType() { - // we write ints in indices() - return com.mojang.blaze3d.IndexType.INT; - } - - public int getIndicesCount() { - return indicesCount; - } - - public int getVertexCount() { - return vertexI; - } - - private int getVerticesOffset() { - return (int) (verticesPtr - verticesPtrStart); - } - - public void validateComplete(String owner) { - debugHasBuffers(owner); - int vertexBytes = getVerticesOffset(); - int expectedVertexBytes = vertexI * vertexStride; - if (vertexBytes != expectedVertexBytes) { - throw new IllegalStateException(owner + ": MeshBuilder vertex stride mismatch: vertexCount=" + vertexI - + ", expectedVertexBytes=" + expectedVertexBytes - + ", actualVertexBytes=" + vertexBytes - + ", stride=" + vertexStride - + ", format=" + format); - } - if (vertexBytes < 0 || vertexBytes > vertices.capacity()) { - throw new IndexOutOfBoundsException(owner + ": MeshBuilder vertex range outside CPU buffer: bytes=" - + vertexBytes + ", capacity=" + vertices.capacity() + ", format=" + format); - } - int indexBytes = indicesCount * Integer.BYTES; - if (indexBytes < 0 || indexBytes > indices.capacity()) { - throw new IndexOutOfBoundsException(owner + ": MeshBuilder index range outside CPU buffer: bytes=" - + indexBytes + ", capacity=" + indices.capacity() + ", format=" + format); - } - if (primitiveIndicesCount > 0 && indicesCount % primitiveIndicesCount != 0) { - if (DEBUG) { - throw new IllegalStateException(owner + ": MeshBuilder incomplete primitive payload: indices=" + indicesCount - + ", primitiveLength=" + primitiveIndicesCount + ", format=" + format); - } else { - int trimmed = (indicesCount / primitiveIndicesCount) * primitiveIndicesCount; - // Silence spam: log once per frame via frame stats? Use logger warn with throttling. - // Trim incomplete tail to avoid Render Frame crash (e.g. 176 % 3 == 2 on TRIANGLES). - org.slf4j.LoggerFactory.getLogger(MeshBuilder.class).warn("{}: trimming incomplete primitive payload: indices={} -> {} (primitiveLength={}, format={})", owner, indicesCount, trimmed, primitiveIndicesCount, format); - indicesCount = trimmed; - if (indicesCount == 0) return; - } - } - debugValidateIndices(owner); - } - - /** - * Releases CPU-side native buffers owned by this builder. The builder may be reused later; - * the next begin()/reserve() call will allocate fresh buffers. - */ - @Override - public void close() { - if (building) { - building = false; - } - freeDirect(vertices); - freeDirect(indices); - vertices = null; - indices = null; - verticesPtrStart = 0L; - verticesPtr = 0L; - indicesPtr = 0L; - vertexI = 0; - indicesCount = 0; - worldCameraAnchored = false; - cameraX = 0.0; - cameraZ = 0.0; - } - - private void debugHasBuffers(String owner) { - if (vertices == null || indices == null) { - throw new IllegalStateException(owner + ": MeshBuilder has no CPU buffers; call begin()/reserve() before writing."); - } - } - - private void debugVertexWriteCapacity(int bytes, String attribute) { - if (!DEBUG) return; - debugHasBuffers("MeshBuilder." + attribute); - if (!building) { - throw new IllegalStateException("MeshBuilder." + attribute + " write outside begin()/end(): format=" + format); - } - if (bytes <= 0) { - throw new IllegalArgumentException("MeshBuilder." + attribute + " invalid write size: " + bytes); - } - int vertexBytes = getVerticesOffset(); - int currentVertexBytes = vertexBytes - vertexI * vertexStride; - if (currentVertexBytes < 0 || currentVertexBytes > vertexStride) { - throw new IllegalStateException("MeshBuilder." + attribute + " cursor is not inside current vertex: vertex=" + vertexI - + ", cursorBytes=" + vertexBytes - + ", currentVertexBytes=" + currentVertexBytes - + ", stride=" + vertexStride - + ", format=" + format); - } - if (currentVertexBytes + bytes > vertexStride) { - throw new IllegalStateException("MeshBuilder." + attribute + " overruns vertex stride: vertex=" + vertexI - + ", currentVertexBytes=" + currentVertexBytes - + ", writeBytes=" + bytes - + ", stride=" + vertexStride - + ", format=" + format); - } - if (vertexBytes + bytes > vertices.capacity()) { - throw new IndexOutOfBoundsException("MeshBuilder." + attribute + " write exceeds vertex CPU buffer: vertex=" + vertexI - + ", cursorBytes=" + vertexBytes - + ", writeBytes=" + bytes - + ", capacity=" + vertices.capacity() - + ", format=" + format); - } - } - - private void debugCompletedVertex() { - if (!DEBUG) return; - debugHasBuffers("MeshBuilder.next"); - int vertexBytes = getVerticesOffset(); - int expected = (vertexI + 1) * vertexStride; - if (vertexBytes != expected) { - throw new IllegalStateException("MeshBuilder.next() called with incomplete/overwritten vertex: vertex=" + vertexI - + ", expectedBytes=" + expected - + ", actualBytes=" + vertexBytes - + ", stride=" + vertexStride - + ", format=" + format); - } - } - - private void debugIndexWriteCapacity(int addIndices, String primitive) { - if (!DEBUG) return; - debugHasBuffers("MeshBuilder." + primitive); - if (!building) { - throw new IllegalStateException("MeshBuilder." + primitive + " index write outside begin()/end(): format=" + format); - } - if (addIndices <= 0) { - throw new IllegalArgumentException("MeshBuilder." + primitive + " invalid index count: " + addIndices); - } - int indexBytes = indicesCount * Integer.BYTES; - int writeBytes = addIndices * Integer.BYTES; - if (indexBytes + writeBytes > indices.capacity()) { - throw new IndexOutOfBoundsException("MeshBuilder." + primitive + " write exceeds index CPU buffer: indices=" + indicesCount - + ", add=" + addIndices - + ", capacityBytes=" + indices.capacity() - + ", format=" + format); - } - } - - private void debugValidateIndices(String owner) { - if (!DEBUG || indicesCount <= 0) return; - long p = indicesPtr; - for (int i = 0; i < indicesCount; i++) { - int index = memGetInt(p + (long) i * Integer.BYTES); - if (index < 0 || index >= vertexI) { - throw new IndexOutOfBoundsException(owner + ": MeshBuilder index references missing vertex: indexOrdinal=" + i - + ", index=" + index - + ", vertexCount=" + vertexI - + ", indicesCount=" + indicesCount - + ", format=" + format); - } - } - } - - public record FrameStatsSnapshot(int initialAllocations, - int vertexReallocs, - int indexReallocs, - long allocatedBytes, - int maxVertexBytes, - int maxIndexBytes) { - } - - private static final class FrameStats { - private int initialAllocations; - private int vertexReallocs; - private int indexReallocs; - private long allocatedBytes; - private int maxVertexBytes; - private int maxIndexBytes; - - private void reset() { - initialAllocations = 0; - vertexReallocs = 0; - indexReallocs = 0; - allocatedBytes = 0L; - maxVertexBytes = 0; - maxIndexBytes = 0; - } - - private FrameStatsSnapshot snapshot() { - return new FrameStatsSnapshot(initialAllocations, vertexReallocs, indexReallocs, allocatedBytes, maxVertexBytes, maxIndexBytes); - } - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/iris/patch/ShaderPatchCompiler.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/iris/patch/ShaderPatchCompiler.java deleted file mode 100644 index 8e2eeb2..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/iris/patch/ShaderPatchCompiler.java +++ /dev/null @@ -1,843 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.render.iris.patch; - -import com.google.common.collect.ImmutableList; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.Reader; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.*; -import java.util.regex.Pattern; - -public enum ShaderPatchCompiler { - ; - private static final Pattern GLOBAL_DECLARATION = Pattern.compile("^(?:flat\\s+|smooth\\s+|noperspective\\s+|centroid\\s+)*(?:in|out|uniform|varying)\\b.*;"); - - public static PatchProgram parse(Reader source) throws IOException, CompileException { - BufferedReader reader = source instanceof BufferedReader buffered ? buffered : new BufferedReader(source); - ArrayList probes = new ArrayList<>(); - ArrayList operations = new ArrayList<>(); - int schemaVersion = 1; - String line; - int lineNumber = 0; - - while ((line = reader.readLine()) != null) { - lineNumber++; - String trimmed = line.trim(); - if (trimmed.isEmpty() || trimmed.startsWith(";")) { - continue; - } - if (!trimmed.startsWith("@")) { - throw new CompileException("Expected directive at line " + lineNumber); - } - - String[] parts = trimmed.substring(1).split("\\s+"); - String directive = parts[0]; - switch (directive) { - case "patch" -> { - if (parts.length != 2) { - throw new CompileException("Invalid patch schema directive at line " + lineNumber); - } - try { - schemaVersion = Integer.parseInt(parts[1]); - } catch (NumberFormatException e) { - throw new CompileException("Invalid patch schema at line " + lineNumber); - } - if (schemaVersion != 1 && schemaVersion != 2) { - throw new CompileException("Unsupported patch schema " + schemaVersion + " at line " + lineNumber); - } - } - case "require" -> probes.add(new Probe(ProbeType.REQUIRE_HASH, readHash(parts, 1, lineNumber))); - case "forbid" -> probes.add(new Probe(ProbeType.FORBID_HASH, readHash(parts, 1, lineNumber))); - case "require_hash" -> probes.add(new Probe(ProbeType.REQUIRE_HASH, readHash(parts, 1, lineNumber))); - case "forbid_hash" -> probes.add(new Probe(ProbeType.FORBID_HASH, readHash(parts, 1, lineNumber))); - case "require_ident" -> probes.add(new Probe(ProbeType.REQUIRE_IDENT, readName(parts, 1, lineNumber))); - case "forbid_ident" -> probes.add(new Probe(ProbeType.FORBID_IDENT, readName(parts, 1, lineNumber))); - case "require_function" -> - probes.add(new Probe(ProbeType.REQUIRE_FUNCTION, readName(parts, 1, lineNumber))); - case "forbid_function" -> - probes.add(new Probe(ProbeType.FORBID_FUNCTION, readName(parts, 1, lineNumber))); - case "require_call" -> probes.add(new Probe(ProbeType.REQUIRE_CALL, readName(parts, 1, lineNumber))); - case "forbid_call" -> probes.add(new Probe(ProbeType.FORBID_CALL, readName(parts, 1, lineNumber))); - case "require_uniform" -> - probes.add(new Probe(ProbeType.REQUIRE_UNIFORM, readName(parts, 1, lineNumber))); - case "forbid_uniform" -> - probes.add(new Probe(ProbeType.FORBID_UNIFORM, readName(parts, 1, lineNumber))); - case "require_define" -> - probes.add(new Probe(ProbeType.REQUIRE_DEFINE, readName(parts, 1, lineNumber))); - case "forbid_define" -> probes.add(new Probe(ProbeType.FORBID_DEFINE, readName(parts, 1, lineNumber))); - case "insert_before", "insert_after", "replace_line", "remove_line" -> { - Selector selector = readHashSelector(parts, 1, lineNumber); - operations.add(new Operation( - OperationType.valueOf(directive.toUpperCase(Locale.ROOT)), - selector, - null, - false, - directive.equals("remove_line") ? List.of() : readPayload(reader, lineNumber) - )); - } - case "replace_range", "replace_between" -> - operations.add(readHashRangeOperation(parts, reader, lineNumber, directive)); - case "insert_zone" -> operations.add(new Operation( - OperationType.INSERT_BEFORE, - readZoneSelector(parts, lineNumber), - null, - false, - readPayload(reader, lineNumber) - )); - case "insert_before_hash", "insert_after_hash", "replace_line_hash", "remove_line_hash" -> - operations.add(readSingleSelectorOperation(parts, reader, lineNumber, directive, SelectorKind.HASH)); - case "insert_before_ident", "insert_after_ident", "replace_line_ident", "remove_line_ident" -> - operations.add(readSingleSelectorOperation(parts, reader, lineNumber, directive, SelectorKind.IDENT)); - case "insert_before_function", "replace_line_function", "remove_line_function" -> - operations.add(readSingleSelectorOperation(parts, reader, lineNumber, directive, SelectorKind.FUNCTION)); - case "insert_after_function_open" -> - operations.add(readSingleSelectorOperation(parts, reader, lineNumber, directive, SelectorKind.FUNCTION_OPEN)); - case "insert_before_define", "insert_after_define", "replace_line_define", "remove_line_define" -> - operations.add(readSingleSelectorOperation(parts, reader, lineNumber, directive, SelectorKind.DEFINE)); - case "insert_before_drawbuffers", "insert_after_drawbuffers", "replace_line_drawbuffers", - "remove_line_drawbuffers" -> - operations.add(readSingleSelectorOperation(parts, reader, lineNumber, directive, SelectorKind.DRAWBUFFERS)); - case "insert_after_stage_begin" -> - operations.add(readSingleSelectorOperation(parts, reader, lineNumber, directive, SelectorKind.STAGE_BEGIN)); - default -> throw new CompileException("Unknown directive @" + directive + " at line " + lineNumber); - } - } - - if (operations.isEmpty()) { - throw new CompileException("Patch contains no operations"); - } - return new PatchProgram(schemaVersion, List.copyOf(probes), List.copyOf(operations)); - } - - public static CompiledPatch compile(PatchProgram program, ImmutableList source) throws CompileException { - return compile(program, source, CompileMode.PREFLIGHT); - } - - public static CompiledPatch compile(PatchProgram program, ImmutableList source, CompileMode mode) throws CompileException { - return compile(program, source, mode, Set.of()); - } - - public static CompiledPatch compile(PatchProgram program, ImmutableList source, CompileMode mode, Set stages) throws CompileException { - if (mode == CompileMode.PREFLIGHT && !containsLoVisualPayload(source)) { - validateNoSourceExcerpt(program, source); - } - - SourceIndex index = SourceIndex.create(source); - validateProbes(program, index); - - ArrayList edits = new ArrayList<>(program.operations().size()); - for (Operation operation : program.operations()) { - int anchor = find(index, operation.anchor()); - switch (operation.type()) { - case INSERT_BEFORE -> edits.add(new Edit(anchor, anchor, operation.payload())); - case INSERT_AFTER -> edits.add(new Edit(anchor + 1, anchor + 1, operation.payload())); - case REPLACE_LINE -> edits.add(new Edit(anchor, anchor + 1, operation.payload())); - case REMOVE_LINE -> edits.add(new Edit(anchor, anchor + 1, List.of())); - case REPLACE_RANGE -> { - int end = find(index, operation.end()); - if (end < anchor) { - throw new CompileException("Range end precedes start"); - } - edits.add(new Edit(anchor, operation.includeEnd() ? end + 1 : end, operation.payload())); - } - case REPLACE_BETWEEN -> { - int end = find(index, operation.end()); - if (end <= anchor) { - throw new CompileException("Range end does not follow start"); - } - edits.add(new Edit(anchor + 1, end, operation.payload())); - } - } - } - - validateNonOverlapping(edits); - validateStageBounds(edits, source, stages); - validateDelimiterDelta(edits, source); - edits.sort(Comparator.comparingInt(Edit::start) - .thenComparingInt(Edit::end) - .reversed()); - return new CompiledPatch(List.copyOf(edits)); - } - - public static ImmutableList apply(CompiledPatch patch, ImmutableList source) { - ArrayList output = new ArrayList<>(source); - for (Edit edit : patch.edits()) { - output.subList(edit.start(), edit.end()).clear(); - output.addAll(edit.start(), edit.payload()); - } - return ImmutableList.copyOf(output); - } - - public static String signature(String sourceLine) { - String normalized = normalizeLine(sourceLine); - try { - MessageDigest digest = MessageDigest.getInstance("SHA-256"); - return HexFormat.of().formatHex(digest.digest(normalized.getBytes(java.nio.charset.StandardCharsets.UTF_8))); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException("SHA-256 unavailable", e); - } - } - - private static Operation readHashRangeOperation(String[] parts, BufferedReader reader, int lineNumber, String directive) throws IOException, CompileException { - if (parts.length < 3 || parts.length > 6) { - throw new CompileException("Invalid " + directive + " at line " + lineNumber); - } - Selector start = new Selector( - SelectorKind.HASH, - validateHash(parts[1], lineNumber), - readPositive(parts, 3, 1, lineNumber), - parts.length <= 3 - ); - Selector end = new Selector( - SelectorKind.HASH, - validateHash(parts[2], lineNumber), - readPositive(parts, 4, 1, lineNumber), - parts.length <= 4 - ); - boolean includeEnd = parts.length >= 6 && Boolean.parseBoolean(parts[5]); - return new Operation( - OperationType.valueOf(directive.toUpperCase(Locale.ROOT)), - start, - end, - includeEnd, - readPayload(reader, lineNumber) - ); - } - - private static Operation readSingleSelectorOperation(String[] parts, - BufferedReader reader, - int lineNumber, - String directive, - SelectorKind kind) throws IOException, CompileException { - OperationType operationType = operationType(directive); - Selector selector = readSelector(parts, kind, lineNumber); - return new Operation( - operationType, - selector, - null, - false, - operationType == OperationType.REMOVE_LINE ? List.of() : readPayload(reader, lineNumber) - ); - } - - private static OperationType operationType(String directive) throws CompileException { - if (directive.startsWith("insert_before")) return OperationType.INSERT_BEFORE; - if (directive.startsWith("insert_after")) return OperationType.INSERT_AFTER; - if (directive.startsWith("replace_line")) return OperationType.REPLACE_LINE; - if (directive.startsWith("remove_line")) return OperationType.REMOVE_LINE; - throw new CompileException("Unsupported operation directive @" + directive); - } - - private static Selector readZoneSelector(String[] parts, int lineNumber) throws CompileException { - if (parts.length != 2) { - throw new CompileException("Invalid insert_zone directive at line " + lineNumber); - } - String zone = parts[1].toLowerCase(Locale.ROOT); - if (!zone.equals("declarations") && !zone.equals("functions")) { - throw new CompileException("Unsupported insert_zone " + parts[1] + " at line " + lineNumber); - } - return new Selector(SelectorKind.ZONE, zone, 1, false); - } - - private static Selector readSelector(String[] parts, SelectorKind kind, int lineNumber) throws CompileException { - if (kind == SelectorKind.DRAWBUFFERS) { - if (parts.length < 1 || parts.length > 2) { - throw new CompileException("Invalid drawbuffers selector at line " + lineNumber); - } - return new Selector(SelectorKind.DRAWBUFFERS, "DRAWBUFFERS", readPositive(parts, 1, 1, lineNumber), parts.length == 1); - } - if (parts.length < 2 || parts.length > 3) { - throw new CompileException("Invalid selector at line " + lineNumber); - } - String value = kind == SelectorKind.HASH ? validateHash(parts[1], lineNumber) : validateName(parts[1], lineNumber); - return new Selector(kind, value, readPositive(parts, 2, 1, lineNumber), parts.length == 2); - } - - private static Selector readHashSelector(String[] parts, int signatureIndex, int lineNumber) throws CompileException { - if (parts.length < signatureIndex + 1 || parts.length > signatureIndex + 2) { - throw new CompileException("Invalid hash anchor at line " + lineNumber); - } - return new Selector( - SelectorKind.HASH, - validateHash(parts[signatureIndex], lineNumber), - readPositive(parts, signatureIndex + 1, 1, lineNumber), - parts.length == signatureIndex + 1 - ); - } - - private static String readHash(String[] parts, int index, int lineNumber) throws CompileException { - if (parts.length != index + 1) { - throw new CompileException("Invalid hash directive at line " + lineNumber); - } - return validateHash(parts[index], lineNumber); - } - - private static String readName(String[] parts, int index, int lineNumber) throws CompileException { - if (parts.length != index + 1) { - throw new CompileException("Invalid identifier directive at line " + lineNumber); - } - return validateName(parts[index], lineNumber); - } - - private static String validateHash(String hash, int lineNumber) throws CompileException { - String normalized = hash.toLowerCase(Locale.ROOT); - if (!normalized.matches("[0-9a-f]{64}")) { - throw new CompileException("Invalid structural hash at line " + lineNumber); - } - return normalized; - } - - private static String validateName(String name, int lineNumber) throws CompileException { - if (!name.matches("[A-Za-z_][A-Za-z0-9_]*")) { - throw new CompileException("Invalid identifier '" + name + "' at line " + lineNumber); - } - return name; - } - - private static int readPositive(String[] parts, int index, int fallback, int lineNumber) throws CompileException { - if (parts.length <= index) { - return fallback; - } - try { - int value = Integer.parseInt(parts[index]); - if (value < 1) { - throw new NumberFormatException(); - } - return value; - } catch (NumberFormatException e) { - throw new CompileException("Invalid occurrence at line " + lineNumber); - } - } - - private static List readPayload(BufferedReader reader, int directiveLine) throws IOException, CompileException { - ArrayList payload = new ArrayList<>(); - String line; - while ((line = reader.readLine()) != null) { - if (line.trim().equals("@end")) { - return List.copyOf(payload); - } - payload.add(line); - } - throw new CompileException("Unterminated payload after line " + directiveLine); - } - - private static void validateProbes(PatchProgram program, SourceIndex index) throws CompileException { - ArrayList missing = new ArrayList<>(); - ArrayList forbidden = new ArrayList<>(); - for (Probe probe : program.probes()) { - boolean present = switch (probe.type()) { - case REQUIRE_HASH, FORBID_HASH -> index.availableHashes().contains(probe.value()); - case REQUIRE_IDENT, FORBID_IDENT -> hasIdentifier(index.source(), probe.value()); - case REQUIRE_FUNCTION, FORBID_FUNCTION -> - findFunctionStart(index.source(), probe.value(), 1, false) >= 0; - case REQUIRE_CALL, FORBID_CALL -> hasCall(index.source(), probe.value()); - case REQUIRE_UNIFORM, FORBID_UNIFORM -> hasUniform(index.source(), probe.value()); - case REQUIRE_DEFINE, FORBID_DEFINE -> hasDefine(index.source(), probe.value()); - }; - if (probe.type().required() && !present) { - missing.add(probe.type().label() + " " + probe.value()); - } else if (!probe.type().required() && present) { - forbidden.add(probe.type().label() + " " + probe.value()); - } - } - if (!missing.isEmpty()) { - throw new CompileException("Required structural probes are absent: " + String.join(", ", missing)); - } - if (!forbidden.isEmpty()) { - throw new CompileException("Forbidden structural probes are present: " + String.join(", ", forbidden)); - } - } - - private static int find(SourceIndex index, Selector selector) throws CompileException { - return switch (selector.kind()) { - case HASH -> findHash(index.hashes(), selector); - case IDENT -> findLine(index.source(), selector, line -> hasIdentifier(line, selector.value())); - case FUNCTION -> findFunctionStartChecked(index.source(), selector); - case FUNCTION_OPEN -> findFunctionOpenChecked(index.source(), selector); - case DEFINE -> findLine(index.source(), selector, line -> isDefine(line, selector.value())); - case DRAWBUFFERS -> findLine(index.source(), selector, line -> line.contains("DRAWBUFFERS")); - case STAGE_BEGIN -> findLine(index.source(), selector, line -> isStageBegin(line, selector.value())); - case ZONE -> findZone(index.source(), selector.value()); - }; - } - - private static int findHash(List hashes, Selector selector) throws CompileException { - int occurrence = 0; - int result = -1; - for (int i = 0; i < hashes.size(); i++) { - if (!hashes.get(i).equals(selector.value())) { - continue; - } - occurrence++; - if (occurrence == selector.occurrence()) { - result = i; - } - } - if (result < 0) { - throw new CompileException("Anchor not found: hash " + selector.value() + "#" + selector.occurrence()); - } - if (selector.requireUnique() && occurrence != 1) { - throw new CompileException("Anchor is ambiguous; specify occurrence: hash " + selector.value()); - } - return result; - } - - private static int findLine(List source, Selector selector, LinePredicate predicate) throws CompileException { - int occurrence = 0; - int result = -1; - for (int i = 0; i < source.size(); i++) { - if (!predicate.matches(source.get(i))) { - continue; - } - occurrence++; - if (occurrence == selector.occurrence()) { - result = i; - } - } - if (result < 0) { - throw new CompileException("Anchor not found: " + selector.kind().name().toLowerCase(Locale.ROOT) + " " + selector.value() + "#" + selector.occurrence()); - } - if (selector.requireUnique() && occurrence != 1) { - throw new CompileException("Anchor is ambiguous; specify occurrence: " + selector.kind().name().toLowerCase(Locale.ROOT) + " " + selector.value()); - } - return result; - } - - private static int findFunctionStartChecked(List source, Selector selector) throws CompileException { - int index = findFunctionStart(source, selector.value(), selector.occurrence(), selector.requireUnique()); - if (index < 0) { - throw new CompileException("Anchor not found: function " + selector.value() + "#" + selector.occurrence()); - } - return index; - } - - private static int findFunctionOpenChecked(List source, Selector selector) throws CompileException { - int start = findFunctionStartChecked(source, new Selector(SelectorKind.FUNCTION, selector.value(), selector.occurrence(), selector.requireUnique())); - for (int i = start; i < source.size(); i++) { - String line = stripLineComment(source.get(i)); - int brace = line.indexOf('{'); - int semicolon = line.indexOf(';'); - if (brace >= 0 && (semicolon < 0 || brace < semicolon)) { - return i; - } - if (semicolon >= 0) { - break; - } - } - throw new CompileException("Function opening brace not found: " + selector.value() + "#" + selector.occurrence()); - } - - private static int findFunctionStart(List source, String functionName, int requestedOccurrence, boolean requireUnique) { - int occurrence = 0; - int result = -1; - for (int i = 0; i < source.size(); i++) { - if (!looksLikeFunctionStart(source.get(i), functionName)) { - continue; - } - occurrence++; - if (occurrence == requestedOccurrence) { - result = i; - } - } - if (result < 0) { - return -1; - } - if (requireUnique && occurrence != 1) { - return -2; - } - return result; - } - - private static int findZone(List source, String zone) { - if (zone.equals("declarations")) { - for (int i = 0; i < source.size(); i++) { - String trimmed = source.get(i).trim(); - if (GLOBAL_DECLARATION.matcher(trimmed).matches()) { - return i; - } - } - } - for (int i = 0; i < source.size(); i++) { - if (looksLikeAnyFunctionStart(source.get(i))) { - return i; - } - } - return 0; - } - - private static boolean isStageBegin(String line, String stageMacro) { - String stripped = stripLineComment(line).trim(); - return stripped.equals("#ifdef " + stageMacro) || stripped.equals("#if defined " + stageMacro) || stripped.equals("#if defined(" + stageMacro + ")"); - } - - private static boolean looksLikeAnyFunctionStart(String line) { - String stripped = stripLineComment(line).trim(); - if (stripped.isEmpty() || stripped.startsWith("#")) return false; - return stripped.matches(".*\\b[A-Za-z_][A-Za-z0-9_]*\\s*\\(.*") && !stripped.endsWith(";"); - } - - private static boolean looksLikeFunctionStart(String line, String functionName) { - String stripped = stripLineComment(line).trim(); - if (stripped.isEmpty() || stripped.startsWith("#")) return false; - if (!hasIdentifier(stripped, functionName)) return false; - int nameIndex = stripped.indexOf(functionName); - int paren = stripped.indexOf('(', nameIndex + functionName.length()); - if (paren < 0) return false; - int semicolon = stripped.indexOf(';'); - return semicolon < 0 || semicolon > paren; - } - - private static boolean hasCall(List source, String name) { - String needle = name + "("; - for (String line : source) { - String stripped = stripLineComment(line); - if (stripped.contains(needle)) { - return true; - } - } - return false; - } - - private static boolean hasUniform(List source, String name) { - for (String line : source) { - String stripped = stripLineComment(line).trim(); - if (stripped.startsWith("uniform ") && hasIdentifier(stripped, name)) { - return true; - } - } - return false; - } - - private static boolean hasDefine(List source, String name) { - for (String line : source) { - if (isDefine(line, name)) { - return true; - } - } - return false; - } - - private static boolean isDefine(String line, String name) { - String stripped = stripLineComment(line).trim(); - if (!stripped.startsWith("#define")) return false; - String rest = stripped.substring("#define".length()).trim(); - if (rest.startsWith(name)) { - return rest.length() == name.length() || !isIdentifierPart(rest.charAt(name.length())); - } - return false; - } - - private static boolean hasIdentifier(List source, String identifier) { - for (String line : source) { - if (hasIdentifier(line, identifier)) { - return true; - } - } - return false; - } - - private static boolean hasIdentifier(String line, String identifier) { - String stripped = stripLineComment(line); - int from = 0; - while (from < stripped.length()) { - int index = stripped.indexOf(identifier, from); - if (index < 0) return false; - int before = index - 1; - int after = index + identifier.length(); - boolean validBefore = before < 0 || !isIdentifierPart(stripped.charAt(before)); - boolean validAfter = after >= stripped.length() || !isIdentifierPart(stripped.charAt(after)); - if (validBefore && validAfter) { - return true; - } - from = index + identifier.length(); - } - return false; - } - - private static boolean isIdentifierPart(char character) { - return Character.isLetterOrDigit(character) || character == '_'; - } - - private static String stripLineComment(String line) { - int comment = line.indexOf("//"); - return comment >= 0 ? line.substring(0, comment) : line; - } - - private static boolean containsLoVisualPayload(List source) { - for (String line : source) { - if (line.contains("lovisual") || line.contains("LOVISUAL_")) { - return true; - } - } - return false; - } - - private static void validateNoSourceExcerpt(PatchProgram program, List source) throws CompileException { - List normalizedSource = source.stream().map(ShaderPatchCompiler::normalizeLine).toList(); - for (Operation operation : program.operations()) { - List payload = operation.payload().stream().map(ShaderPatchCompiler::normalizeLine).toList(); - for (int payloadStart = 0; payloadStart + 2 < payload.size(); payloadStart++) { - if (payload.get(payloadStart).isEmpty() - || payload.get(payloadStart + 1).isEmpty() - || payload.get(payloadStart + 2).isEmpty()) { - continue; - } - for (int sourceStart = 0; sourceStart + 2 < normalizedSource.size(); sourceStart++) { - if (payload.get(payloadStart).equals(normalizedSource.get(sourceStart)) - && payload.get(payloadStart + 1).equals(normalizedSource.get(sourceStart + 1)) - && payload.get(payloadStart + 2).equals(normalizedSource.get(sourceStart + 2))) { - throw new CompileException("Patch payload reproduces a target source excerpt"); - } - } - } - } - } - - private static String normalizeLine(String sourceLine) { - String source = sourceLine == null ? "" : sourceLine; - StringBuilder normalized = new StringBuilder(source.length()); - for (int i = 0; i < source.length(); i++) { - char character = source.charAt(i); - if (!Character.isWhitespace(character)) { - normalized.append(character); - } - } - return normalized.toString(); - } - - private static void validateNonOverlapping(List edits) throws CompileException { - ArrayList sorted = new ArrayList<>(edits); - sorted.sort(Comparator.comparingInt(Edit::start).thenComparingInt(Edit::end)); - int previousEnd = -1; - for (Edit edit : sorted) { - if (edit.start() < previousEnd) { - throw new CompileException("Patch operations overlap"); - } - previousEnd = Math.max(previousEnd, edit.end()); - } - } - - private static void validateStageBounds(List edits, List source, Set stages) throws CompileException { - if (stages == null || stages.isEmpty() || stages.contains(ShaderStage.ANY) || !sourceHasStageBlocks(source)) { - return; - } - for (Edit edit : edits) { - ShaderStage stage = stageBeforeLine(source, edit.start()); - if (stage == null || !stages.contains(stage)) { - throw new CompileException("Patch edit escapes allowed shader stage(s): editLine=" + (edit.start() + 1) + " stage=" + (stage == null ? "none" : stage.name().toLowerCase(Locale.ROOT)) + " allowed=" + stages); - } - } - } - - private static boolean sourceHasStageBlocks(List source) { - for (String line : source) { - String stripped = stripLineComment(line).trim(); - if (stripped.contains("FRAGMENT_SHADER") || stripped.contains("VERTEX_SHADER") || stripped.contains("GEOMETRY_SHADER") || stripped.contains("COMPUTE_SHADER")) { - return true; - } - } - return false; - } - - private static ShaderStage stageBeforeLine(List source, int lineIndex) { - ArrayList stack = new ArrayList<>(); - int end = Math.min(Math.max(lineIndex, 0), source.size()); - for (int i = 0; i < end; i++) { - String stripped = stripLineComment(source.get(i)).trim(); - ShaderStage begin = stageFromDirective(stripped); - if (begin != null) { - stack.add(begin); - } else if (stripped.startsWith("#endif") && !stack.isEmpty()) { - stack.remove(stack.size() - 1); - } - } - for (int i = stack.size() - 1; i >= 0; i--) { - ShaderStage stage = stack.get(i); - if (stage != ShaderStage.ANY) { - return stage; - } - } - return null; - } - - private static ShaderStage stageFromDirective(String stripped) { - if (!stripped.startsWith("#if")) { - return null; - } - if (stripped.contains("FRAGMENT_SHADER")) return ShaderStage.FRAGMENT; - if (stripped.contains("VERTEX_SHADER")) return ShaderStage.VERTEX; - if (stripped.contains("GEOMETRY_SHADER")) return ShaderStage.GEOMETRY; - if (stripped.contains("COMPUTE_SHADER")) return ShaderStage.COMPUTE; - return null; - } - - private static void validateDelimiterDelta(List edits, List source) throws CompileException { - for (char open : new char[]{'{', '(', '['}) { - char close = switch (open) { - case '{' -> '}'; - case '(' -> ')'; - case '[' -> ']'; - default -> throw new IllegalStateException(); - }; - int delta = 0; - for (Edit edit : edits) { - delta += delimiterBalance(edit.payload(), open, close); - delta -= delimiterBalance(source.subList(edit.start(), edit.end()), open, close); - } - if (delta != 0) { - throw new CompileException("Patch changes " + open + close + " delimiter balance"); - } - } - } - - private static int delimiterBalance(List lines, char open, char close) { - int balance = 0; - for (String line : lines) { - for (int i = 0; i < line.length(); i++) { - char character = line.charAt(i); - if (character == open) balance++; - if (character == close) balance--; - } - } - return balance; - } - - public enum CompileMode { - PREFLIGHT, - APPLICATION - } - - public enum ShaderStage { - ANY, - FRAGMENT, - VERTEX, - GEOMETRY, - COMPUTE; - - public static ShaderStage parse(String value) throws CompileException { - if (value == null || value.isBlank()) { - return ANY; - } - try { - return ShaderStage.valueOf(value.trim().toUpperCase(Locale.ROOT)); - } catch (IllegalArgumentException e) { - throw new CompileException("Unsupported shader stage '" + value + "'"); - } - } - } - - public enum ProbeType { - REQUIRE_HASH(true, "hash"), - FORBID_HASH(false, "hash"), - REQUIRE_IDENT(true, "ident"), - FORBID_IDENT(false, "ident"), - REQUIRE_FUNCTION(true, "function"), - FORBID_FUNCTION(false, "function"), - REQUIRE_CALL(true, "call"), - FORBID_CALL(false, "call"), - REQUIRE_UNIFORM(true, "uniform"), - FORBID_UNIFORM(false, "uniform"), - REQUIRE_DEFINE(true, "define"), - FORBID_DEFINE(false, "define"); - - private final boolean required; - private final String label; - - ProbeType(boolean required, String label) { - this.required = required; - this.label = label; - } - - private boolean required() { - return required; - } - - private String label() { - return label; - } - } - - public enum SelectorKind { - HASH, - IDENT, - FUNCTION, - FUNCTION_OPEN, - DEFINE, - DRAWBUFFERS, - STAGE_BEGIN, - ZONE - } - - public enum OperationType { - INSERT_BEFORE, - INSERT_AFTER, - REPLACE_LINE, - REMOVE_LINE, - REPLACE_RANGE, - REPLACE_BETWEEN - } - - private interface LinePredicate { - boolean matches(String line); - } - - public record PatchProgram( - int schemaVersion, - List probes, - List operations - ) { - } - - public record CompiledPatch(List edits) { - } - - public record Edit(int start, int end, List payload) { - } - - public record Probe(ProbeType type, String value) { - } - - public record Selector(SelectorKind kind, String value, int occurrence, boolean requireUnique) { - } - - public record Operation( - OperationType type, - Selector anchor, - Selector end, - boolean includeEnd, - List payload - ) { - } - - private record SourceIndex( - ImmutableList source, - List hashes, - Set availableHashes - ) { - private static SourceIndex create(ImmutableList source) { - ArrayList hashes = new ArrayList<>(source.size()); - Set available = new HashSet<>(source.size()); - for (String line : source) { - String hash = signature(line); - hashes.add(hash); - available.add(hash); - } - return new SourceIndex(source, List.copyOf(hashes), Set.copyOf(available)); - } - } - - public static final class CompileException extends Exception { - public CompileException(String message) { - super(message); - } - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/simulation/PlayerMovementSimulation.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/simulation/PlayerMovementSimulation.java deleted file mode 100644 index 3952a30..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/simulation/PlayerMovementSimulation.java +++ /dev/null @@ -1,757 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.util.player.simulation; - -import net.minecraft.core.BlockPos; -import net.minecraft.tags.BlockTags; -import net.minecraft.tags.FluidTags; -import net.minecraft.util.Mth; -import net.minecraft.world.effect.MobEffectInstance; -import net.minecraft.world.effect.MobEffects; -import net.minecraft.world.entity.Entity; -import net.minecraft.world.entity.EntityDimensions; -import net.minecraft.world.entity.ai.attributes.Attributes; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.level.Level; -import net.minecraft.world.level.block.Blocks; -import net.minecraft.world.level.block.BubbleColumnBlock; -import net.minecraft.world.level.block.LadderBlock; -import net.minecraft.world.level.block.TrapDoorBlock; -import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.level.chunk.LevelChunk; -import net.minecraft.world.level.material.FluidState; -import net.minecraft.world.phys.AABB; -import net.minecraft.world.phys.Vec3; -import dev.loki.lovisual.mixins.accessors.entity.EntityInvoker; -import dev.loki.lovisual.util.player.effect.tracker.StatusEffectAccess; - -import java.util.List; -import dev.loki.lovisual.util.player.simulation.remote.RemotePlayerObservation; -import dev.loki.lovisual.util.player.simulation.SimulatedInput; - -public abstract class PlayerMovementSimulation { - private static final double STEP_HEIGHT = 0.5; - - private final Player player; - private final SimulatedInput input; - private final float yaw; - private final float pitch; - private final boolean sprinting; - private Vec3 pos; - private Vec3 velocity; - private AABB boundingBox; - private boolean onGround; - private boolean jumping; - private boolean gliding; - private double fallDistance; - private boolean horizontalCollision; - private boolean clipLedged; - private boolean touchingWater; - private boolean submergedInWater; - private boolean swimming; - private double waterHeight; - private double lavaHeight; - private int jumpingCooldown; - - protected PlayerMovementSimulation(Player player, SimulatedInput input) { - this.player = player; - this.pos = player.position(); - this.velocity = this.pos.subtract(player.xo, player.yo, player.zo); - this.boundingBox = player.getBoundingBox(); - this.yaw = player.getYRot(); - this.pitch = player.getXRot(); - this.sprinting = player.isSprinting(); - this.onGround = player.onGround(); - this.fallDistance = player.fallDistance; - this.jumping = !player.onGround(); - this.gliding = player.isFallFlying(); - this.input = input; - } - - void tick() { - clipLedged = false; - input.update(); - - updateWaterState(); - updateSubmergedInWaterState(); - updateSwimming(); - applyBubbleColumnEffects(); - - if (jumpingCooldown > 0) { - jumpingCooldown--; - } - - jumping = input.jump; - - double vx = Math.abs(velocity.x) < 0.003 ? 0.0 : velocity.x; - double vy = Math.abs(velocity.y) < 0.003 ? 0.0 : velocity.y; - double vz = Math.abs(velocity.z) < 0.003 ? 0.0 : velocity.z; - velocity = new Vec3(vx, vy, vz); - - if (jumping && shouldSwimInFluids()) { - double fluidHeight = isInLava() ? lavaHeight : waterHeight; - boolean inWater = touchingWater && fluidHeight > 0.0; - double swimHeight = getSwimHeight(); - - if (!inWater || onGround && !(fluidHeight > swimHeight)) { - if (!isInLava() || onGround && !(fluidHeight > swimHeight)) { - if ((onGround || inWater && fluidHeight <= swimHeight) && jumpingCooldown == 0) { - jump(); - jumpingCooldown = 10; - } - } else { - swimUpward(); - } - } else { - swimUpward(); - } - } else { - jumpingCooldown = 0; - } - - if (StatusEffectAccess.has(player, MobEffects.SLOW_FALLING) || StatusEffectAccess.has(player, MobEffects.LEVITATION)) { - onGround = false; - } - - travel(new Vec3(input.movementSideways * 0.98f, 0.0, input.movementForward * 0.98f)); - } - - private void jump() { - double jumpVelocity = player.getAttributeValue(Attributes.JUMP_STRENGTH) * getJumpVelocityMultiplier() - + player.getJumpBoostPower(); - if (jumpVelocity <= 1.0E-5) { - return; - } - - velocity = new Vec3(velocity.x, Math.max(jumpVelocity, velocity.y), velocity.z); - - if (sprinting) { - float yawRadians = yaw * Mth.DEG_TO_RAD; - velocity = velocity.add(-Mth.sin(yawRadians) * 0.2f, 0.0, Mth.cos(yawRadians) * 0.2f); - } - } - - private double getJumpVelocityMultiplier() { - BlockPos feetPos = BlockPos.containing(pos); - float feet = player.level().getBlockState(feetPos).getBlock().getJumpFactor(); - float floor = player.level().getBlockState(getVelocityAffectingPos()).getBlock().getJumpFactor(); - return feet == 1.0f ? floor : feet; - } - - private void travel(Vec3 movementInput) { - if (swimming && !player.isPassenger()) { - double lookY = getRotationVector().y; - double climb = lookY < -0.2 ? 0.085 : 0.06; - BlockPos fluidPos = BlockPos.containing(pos.x, pos.y + 0.9, pos.z); - if (lookY <= 0.0 || jumping || !player.level().getFluidState(fluidPos).isEmpty()) { - velocity = velocity.add(0.0, (lookY - velocity.y) * climb, 0.0); - } - } - - if ((touchingWater || isInLava()) && shouldSwimInFluids()) { - travelInFluid(movementInput); - return; - } - - if (shouldUseRemoteFlightPhysics()) { - travelRemoteFlight(movementInput); - return; - } - - if (gliding) { - travelGliding(movementInput); - return; - } - - travelMidAir(movementInput); - } - - protected boolean shouldUseRemoteFlightPhysics() { - return false; - } - - protected boolean shouldHoldVerticalMotion() { - return false; - } - - protected RemotePlayerObservation.MotionKind motionKind() { - return RemotePlayerObservation.MotionKind.NORMAL; - } - - private void travelRemoteFlight(Vec3 movementInput) { - updateVelocity(0.045f, movementInput); - - if (shouldHoldVerticalMotion() && Math.abs(velocity.y) < 0.12) { - velocity = new Vec3(velocity.x, 0.0, velocity.z); - } - - move(velocity); - - double yDrag = shouldHoldVerticalMotion() ? 0.35 : 0.91; - velocity = velocity.multiply(0.91, yDrag, 0.91); - - if (shouldHoldVerticalMotion() && Math.abs(velocity.y) < 0.015) { - velocity = new Vec3(velocity.x, 0.0, velocity.z); - } - - fallDistance = 0.0; - onGround = false; - } - - private void travelMidAir(Vec3 movementInput) { - BlockPos blockPos = getVelocityAffectingPos(); - float slipperiness = player.level().getBlockState(blockPos).getBlock().getFriction(); - float friction = onGround ? slipperiness * 0.91f : 0.91f; - - Vec3 movedVelocity = applyMovementInput(movementInput, slipperiness); - double y = movedVelocity.y; - MobEffectInstance levitation = StatusEffectAccess.get(player, MobEffects.LEVITATION); - if (levitation != null) { - y += (0.05 * (levitation.getAmplifier() + 1) - movedVelocity.y) * 0.2; - } else if (!player.level().isClientSide() || player.level().hasChunkAt(blockPos)) { - y -= getEffectiveGravity(); - } else if (pos.y > player.level().getMinY()) { - y = -0.1; - } else { - y = 0.0; - } - - if (player.shouldDiscardFriction()) { - velocity = new Vec3(movedVelocity.x, y, movedVelocity.z); - } else { - velocity = new Vec3(movedVelocity.x * friction, y * 0.9800000190734863, movedVelocity.z * friction); - } - } - - private void travelInFluid(Vec3 movementInput) { - boolean falling = velocity.y <= 0.0; - double y = pos.y; - double gravity = getEffectiveGravity(); - - if (touchingWater) { - travelInWater(movementInput, gravity, falling, y); - } else { - travelInLava(movementInput, gravity, falling, y); - } - } - - private void travelGliding(Vec3 movementInput) { - if (isClimbing()) { - travelMidAir(movementInput); - gliding = false; - return; - } - - Vec3 oldVelocity = velocity; - double horizontalSpeed = oldVelocity.horizontalDistance(); - velocity = calcGlidingVelocity(oldVelocity); - move(velocity); - - if (!player.level().isClientSide()) { - double newHorizontalSpeed = velocity.horizontalDistance(); - if (horizontalCollision && oldVelocity.horizontalDistance() - newHorizontalSpeed > 0.0) { - gliding = false; - } - } - } - - private Vec3 calcGlidingVelocity(Vec3 oldVelocity) { - Vec3 rotation = getRotationVector(); - float pitchRadians = pitch * Mth.DEG_TO_RAD; - double horizontalLook = Math.sqrt(rotation.x * rotation.x + rotation.z * rotation.z); - double horizontalVelocity = oldVelocity.horizontalDistance(); - double gravity = getEffectiveGravity(); - double lift = Mth.square(Math.cos(pitchRadians)); - - oldVelocity = oldVelocity.add(0.0, gravity * (-1.0 + lift * 0.75), 0.0); - if (oldVelocity.y < 0.0 && horizontalLook > 0.0) { - double dive = oldVelocity.y * -0.1 * lift; - oldVelocity = oldVelocity.add(rotation.x * dive / horizontalLook, dive, rotation.z * dive / horizontalLook); - } - - if (pitchRadians < 0.0f && horizontalLook > 0.0) { - double climb = horizontalVelocity * -Mth.sin(pitchRadians) * 0.04; - oldVelocity = oldVelocity.add(-rotation.x * climb / horizontalLook, climb * 3.2, -rotation.z * climb / horizontalLook); - } - - if (horizontalLook > 0.0) { - oldVelocity = oldVelocity.add( - (rotation.x / horizontalLook * horizontalVelocity - oldVelocity.x) * 0.1, - 0.0, - (rotation.z / horizontalLook * horizontalVelocity - oldVelocity.z) * 0.1 - ); - } - - return oldVelocity.multiply(0.99f, 0.98f, 0.99f); - } - - private void travelInWater(Vec3 movementInput, double gravity, boolean falling, double y) { - float movementMultiplier = sprinting ? 0.9f : 0.8f; - float acceleration = 0.02f; - float waterEfficiency = (float) player.getAttributeValue(Attributes.WATER_MOVEMENT_EFFICIENCY); - if (!onGround) { - waterEfficiency *= 0.5f; - } - - if (waterEfficiency > 0.0f) { - movementMultiplier += (0.54600006f - movementMultiplier) * waterEfficiency; - acceleration += (player.getSpeed() - acceleration) * waterEfficiency; - } - - if (StatusEffectAccess.has(player, MobEffects.DOLPHINS_GRACE)) { - movementMultiplier = 0.96f; - } - - updateVelocity(acceleration, movementInput); - move(velocity); - - Vec3 movedVelocity = velocity; - if (horizontalCollision && isClimbing()) { - movedVelocity = new Vec3(movedVelocity.x, 0.2, movedVelocity.z); - } - - movedVelocity = movedVelocity.multiply(movementMultiplier, 0.8f, movementMultiplier); - velocity = applyFluidMovingSpeed(gravity, falling, movedVelocity); - resetVerticalVelocityInFluid(y); - } - - private void travelInLava(Vec3 movementInput, double gravity, boolean falling, double y) { - updateVelocity(0.02f, movementInput); - move(velocity); - if (lavaHeight <= getSwimHeight()) { - velocity = velocity.multiply(0.5, 0.8f, 0.5); - velocity = applyFluidMovingSpeed(gravity, falling, velocity); - } else { - velocity = velocity.scale(0.5); - } - - if (gravity != 0.0) { - velocity = velocity.add(0.0, -gravity / 4.0, 0.0); - } - - resetVerticalVelocityInFluid(y); - } - - private float getMovementSpeed(float slipperiness) { - if (onGround) { - return player.getSpeed() * (0.21600002f / (slipperiness * slipperiness * slipperiness)); - } - - return sprinting ? 0.025999999f : 0.02f; - } - - private void updateVelocity(float speed, Vec3 movementInput) { - Vec3 added = EntityInvoker.lovisual$movementInputToVelocity(movementInput, speed, yaw); - velocity = velocity.add(added); - } - - private Vec3 applyMovementInput(Vec3 movementInput, float slipperiness) { - updateVelocity(getMovementSpeed(slipperiness), movementInput); - velocity = applyClimbingSpeed(velocity); - velocity = applyWebSpeed(velocity); - move(adjustMovementForSneaking(velocity)); - Vec3 movedVelocity = velocity; - if ((horizontalCollision || jumping) && isClimbing()) { - movedVelocity = new Vec3(movedVelocity.x, 0.2, movedVelocity.z); - } - return movedVelocity; - } - - private Vec3 adjustMovementForSneaking(Vec3 movement) { - if (movement.y > 0.0 || !shouldConsiderLedgeClipping()) { - return movement; - } - - double x = movement.x; - double z = movement.z; - while (x != 0.0 && player.level().noCollision(player, boundingBox.move(x, -STEP_HEIGHT, 0.0))) { - if (x < 0.05 && x >= -0.05) { - x = 0.0; - } else if (x > 0.0) { - x -= 0.05; - } else { - x += 0.05; - } - } - - while (z != 0.0 && player.level().noCollision(player, boundingBox.move(0.0, -STEP_HEIGHT, z))) { - if (z < 0.05 && z >= -0.05) { - z = 0.0; - } else if (z > 0.0) { - z -= 0.05; - } else { - z += 0.05; - } - } - - while (x != 0.0 && z != 0.0 && player.level().noCollision(player, boundingBox.move(x, -STEP_HEIGHT, z))) { - if (x < 0.05 && x >= -0.05) { - x = 0.0; - } else if (x > 0.0) { - x -= 0.05; - } else { - x += 0.05; - } - - if (z < 0.05 && z >= -0.05) { - z = 0.0; - } else if (z > 0.0) { - z -= 0.05; - } else { - z += 0.05; - } - } - - if (movement.x != x || movement.z != z) { - clipLedged = true; - } - - if (shouldClipAtLedge()) { - return new Vec3(x, movement.y, z); - } - - return movement; - } - - private boolean shouldClipAtLedge() { - return !input.ignoreClippingAtLedge && (input.sneak || input.forceSafeWalk); - } - - private boolean shouldConsiderLedgeClipping() { - return onGround || fallDistance < STEP_HEIGHT && !player.level().noCollision( - player, - boundingBox.move(0.0, fallDistance - STEP_HEIGHT, 0.0) - ); - } - - private Vec3 applyClimbingSpeed(Vec3 motion) { - if (!isClimbing()) { - return motion; - } - - double x = Mth.clamp(motion.x, -0.15f, 0.15f); - double z = Mth.clamp(motion.z, -0.15f, 0.15f); - double y = Math.max(motion.y, -0.15f); - if (y < 0.0 && player.isShiftKeyDown()) { - y = 0.0; - } - return new Vec3(x, y, z); - } - - private void move(Vec3 movement) { - Vec3 adjusted = movement.lengthSqr() == 0.0 - ? movement - : Entity.collideBoundingBox(player, movement, boundingBox, player.level(), List.of()); - - if (adjusted.lengthSqr() > 1.0E-7 || movement.lengthSqr() - adjusted.lengthSqr() < 1.0E-7) { - pos = pos.add(adjusted); - EntityDimensions dimensions = player.getDimensions(player.getPose()); - boundingBox = dimensions.makeBoundingBox(pos); - } - - boolean xCollision = movement.x != adjusted.x; - boolean yCollision = movement.y != adjusted.y; - boolean zCollision = movement.z != adjusted.z; - horizontalCollision = xCollision || zCollision; - onGround = yCollision && movement.y < 0.0; - if (onGround) { - fallDistance = 0.0; - } else if (adjusted.y < 0.0) { - fallDistance -= adjusted.y; - } - - if (xCollision || zCollision || yCollision) { - velocity = new Vec3( - xCollision ? 0.0 : velocity.x, - yCollision ? 0.0 : velocity.y, - zCollision ? 0.0 : velocity.z - ); - } - - float velocityMultiplier = getVelocityMultiplier(); - velocity = new Vec3(velocity.x * velocityMultiplier, velocity.y, velocity.z * velocityMultiplier); - } - - private BlockPos getVelocityAffectingPos() { - return BlockPos.containing(pos.x, boundingBox.minY - 0.5000001, pos.z); - } - - private double getEffectiveGravity() { - double gravity = player.getGravity(); - if (velocity.y <= 0.0 && StatusEffectAccess.has(player, MobEffects.SLOW_FALLING)) { - return Math.min(gravity, 0.01); - } - return gravity; - } - - private boolean shouldSwimInFluids() { - return !player.getAbilities().flying; - } - - private boolean isInLava() { - return lavaHeight > 0.0; - } - - private double getSwimHeight() { - return player.getEyeHeight(player.getPose()) < 0.4f ? 0.0 : 0.4; - } - - private boolean isClimbing() { - if (player.getAbilities().flying || player.isSpectator()) { - return false; - } - - BlockPos blockPos = BlockPos.containing(pos); - BlockState blockState = player.level().getBlockState(blockPos); - if (gliding && blockState.is(BlockTags.CAN_GLIDE_THROUGH)) { - return false; - } - if (blockState.is(BlockTags.CLIMBABLE)) { - return true; - } - return blockState.getBlock() instanceof TrapDoorBlock && canEnterTrapdoor(blockPos, blockState); - } - - private Vec3 applyWebSpeed(Vec3 motion) { - BlockState state = player.level().getBlockState(BlockPos.containing(pos)); - if (!state.is(Blocks.COBWEB)) { - return motion; - } - - if (StatusEffectAccess.has(player, MobEffects.WEAVING)) { - return motion.multiply(0.5, 0.25, 0.5); - } - - return motion.multiply(0.25, 0.05, 0.25); - } - - private void swimUpward() { - velocity = velocity.add(0.0, 0.04f, 0.0); - } - - private Vec3 applyFluidMovingSpeed(double gravity, boolean falling, Vec3 motion) { - if (gravity != 0.0 && !sprinting) { - double y = falling && Math.abs(motion.y - 0.005) >= 0.003 && Math.abs(motion.y - gravity / 16.0) < 0.003 - ? -0.003 - : motion.y - gravity / 16.0; - return new Vec3(motion.x, y, motion.z); - } - - return motion; - } - - private void resetVerticalVelocityInFluid(double y) { - if (horizontalCollision && doesNotCollide(velocity.x, velocity.y + 0.6f - pos.y + y, velocity.z)) { - velocity = new Vec3(velocity.x, 0.3f, velocity.z); - } - } - - private boolean doesNotCollide(double offsetX, double offsetY, double offsetZ) { - AABB moved = boundingBox.move(offsetX, offsetY, offsetZ); - return player.level().noCollision(player, moved) && !player.level().containsAnyLiquid(moved); - } - - private void updateWaterState() { - waterHeight = 0.0; - lavaHeight = 0.0; - touchingWater = updateMovementInFluid(true, 0.014); - updateMovementInFluid(false, 0.0023333333333333335); - } - - private void updateSubmergedInWaterState() { - double eyeY = pos.y + player.getEyeHeight(player.getPose()); - BlockPos blockPos = BlockPos.containing(pos.x, eyeY, pos.z); - FluidState fluidState = player.level().getFluidState(blockPos); - double fluidY = blockPos.getY() + fluidState.getHeight(player.level(), blockPos); - submergedInWater = fluidState.is(FluidTags.WATER) && fluidY > eyeY; - } - - private void updateSwimming() { - if (player.getAbilities().flying) { - swimming = false; - return; - } - - if (swimming) { - swimming = sprinting && touchingWater && !player.isPassenger(); - } else { - FluidState state = player.level().getFluidState(BlockPos.containing(pos)); - swimming = sprinting && submergedInWater && !player.isPassenger() && state.is(FluidTags.WATER); - } - } - - private boolean updateMovementInFluid(boolean water, double speed) { - Level world = player.level(); - if (isRegionUnloaded(world)) { - return false; - } - - AABB box = boundingBox.deflate(0.001); - int minX = Mth.floor(box.minX); - int maxX = Mth.ceil(box.maxX); - int minY = Mth.floor(box.minY); - int maxY = Mth.ceil(box.maxY); - int minZ = Mth.floor(box.minZ); - int maxZ = Mth.ceil(box.maxZ); - - double maxHeight = 0.0; - boolean found = false; - Vec3 addedVelocity = Vec3.ZERO; - int samples = 0; - BlockPos.MutableBlockPos mutable = new BlockPos.MutableBlockPos(); - - for (int x = minX; x < maxX; x++) { - for (int y = minY; y < maxY; y++) { - for (int z = minZ; z < maxZ; z++) { - mutable.set(x, y, z); - FluidState fluidState = world.getFluidState(mutable); - if (!(water ? fluidState.is(FluidTags.WATER) : fluidState.is(FluidTags.LAVA))) { - continue; - } - - double height = y + fluidState.getHeight(world, mutable); - if (height < box.minY) { - continue; - } - - found = true; - maxHeight = Math.max(height - box.minY, maxHeight); - Vec3 fluidVelocity = fluidState.getFlow(world, mutable); - if (maxHeight < 0.4) { - fluidVelocity = fluidVelocity.scale(maxHeight); - } - addedVelocity = addedVelocity.add(fluidVelocity); - samples++; - } - } - } - - if (samples > 0) { - addedVelocity = addedVelocity.scale(1.0 / samples).scale(speed); - if (Math.abs(velocity.x) < 0.003 && Math.abs(velocity.z) < 0.003 && addedVelocity.length() < 0.0045) { - addedVelocity = addedVelocity.normalize().scale(0.0045); - } - if (addedVelocity.lengthSqr() > 0.0) { - velocity = velocity.add(addedVelocity); - } - } - - if (water) { - waterHeight = maxHeight; - } else { - lavaHeight = maxHeight; - } - return found; - } - - private boolean isRegionUnloaded(Level world) { - AABB box = boundingBox.inflate(1.0); - int minX = Mth.floor(box.minX); - int maxX = Mth.ceil(box.maxX); - int minZ = Mth.floor(box.minZ); - int maxZ = Mth.ceil(box.maxZ); - - for (int x = minX >> 4; x <= (maxX - 1) >> 4; x++) { - for (int z = minZ >> 4; z <= (maxZ - 1) >> 4; z++) { - LevelChunk chunk = world.getChunkSource().getChunkNow(x, z); - if (chunk == null) { - return true; - } - } - } - return false; - } - - private void applyBubbleColumnEffects() { - BlockPos pos = BlockPos.containing(this.pos); - BlockState state = player.level().getBlockState(pos); - if (!state.is(Blocks.BUBBLE_COLUMN)) { - return; - } - - BlockState above = player.level().getBlockState(pos.above()); - boolean surface = above.getCollisionShape(player.level(), pos).isEmpty() && above.getFluidState().isEmpty(); - boolean drag = state.getValue(BubbleColumnBlock.DRAG_DOWN); - - double y; - if (surface) { - y = drag ? Math.max(-0.9, velocity.y - 0.03) : Math.min(1.8, velocity.y + 0.1); - } else { - y = drag ? Math.max(-0.3, velocity.y - 0.03) : Math.min(0.7, velocity.y + 0.06); - onGround = true; - } - - velocity = new Vec3(velocity.x, y, velocity.z); - } - - private float getVelocityMultiplier() { - if (player.getAbilities().flying || gliding) { - return 1.0f; - } - - BlockState state = player.level().getBlockState(BlockPos.containing(pos)); - float multiplier = state.getBlock().getSpeedFactor(); - if (!state.is(Blocks.WATER) && !state.is(Blocks.BUBBLE_COLUMN)) { - if (multiplier == 1.0f) { - multiplier = player.level().getBlockState(getVelocityAffectingPos()).getBlock().getSpeedFactor(); - } - } - return multiplier; - } - - private boolean canEnterTrapdoor(BlockPos pos, BlockState state) { - if (!(Boolean) state.getValue(TrapDoorBlock.OPEN)) { - return false; - } - - BlockState belowState = player.level().getBlockState(pos.below()); - return belowState.is(Blocks.LADDER) && belowState.getValue(LadderBlock.FACING) == state.getValue(TrapDoorBlock.FACING); - } - - private Vec3 getRotationVector() { - return Vec3.directionFromRotation(pitch, yaw); - } - - protected void setSimulatedVelocity(Vec3 velocity) { - this.velocity = velocity; - } - - protected void setSimulatedFallDistance(double fallDistance) { - this.fallDistance = fallDistance; - } - - protected void setSimulatedOnGround(boolean onGround) { - this.onGround = onGround; - } - - protected void setSimulatedGliding(boolean gliding) { - this.gliding = gliding; - } - - Vec3 position() { - return pos; - } - - double simulatedFallDistance() { - return fallDistance; - } - - Vec3 simulatedVelocity() { - return velocity; - } - - boolean simulatedOnGround() { - return onGround; - } - - boolean simulatedClipLedged() { - return clipLedged; - } - -} - diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..ef92243 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,5 @@ +# docs + +- `DEV_GUIDE.md` — единый справочник разработчика (сборка, правила ≤200/≤4, пакеты, модули/HUD/команды/миксины/рендер/тесты). Объединён из `DEV_GUIDE.md`+`DEVELOPMENT.md` 2026-09-23. +- `api/addons.md` — API аддонов (`lovisual:addon`, жизненный цикл, что может аддон). Публичный API можно ломать свободно при рефакторинге (KISS > совместимость). +- `TODO.md` в корне — план фаз (сейчас 8.5/9.1/9.2/10/11). diff --git a/docs/api/addons.md b/docs/api/addons.md new file mode 100644 index 0000000..db4c63c --- /dev/null +++ b/docs/api/addons.md @@ -0,0 +1,73 @@ +# LoVisual Addon API (`api/v0`) + +Аддоны — обычные Fabric-моды, которые расширяют LoVisual. Точка входа `lovisual:addon` в `fabric.mod.json`. Можно свободно менять публичный API при рефакторинге — владелец разрешил ломать совместимость ради KISS. + +## Entrypoint + +`fabric.mod.json`: +```json +{ + "schemaVersion": 1, + "id": "my-addon", + "name": "My Addon", + "entrypoints": { + "lovisual:addon": ["com.example.MyAddon"] + }, + "custom": { + "lovisual:addon": { "api": 0, "icon": "assets/my-addon/icon.png" } + } +} +``` + +Класс: +```java +public class MyAddon implements LoVisualAddon { + public void onConfigureModules(LoVisualModuleLoadContext ctx) { + ctx.disableBuiltInModule("killAura"); // выключить встроенный модуль до его создания + } + public void onInitialize(LoVisualAddonContext ctx) { + ctx.registerModule(MyModule.class); + ctx.registerDraggableHudElement(MyHud.class); + ctx.registerStaticHudElement(MyStaticHud.class); + ctx.registerCommand(new MyCommand()); + ctx.registerClickGuiSection(new MySection()); + ctx.addRenderCallback(LoVisualRenderStage.HUD_RAW, (c) -> { /* 2D */ }); + } + public void onClientReady(LoVisualAddonRuntimeContext ctx) {} + public void onShutdown(LoVisualAddonRuntimeContext ctx) {} + public void onRuntimeSuspended(LoVisualAddonRuntimeContext ctx) {} // при выкл. аддона + public void onRuntimeResumed(LoVisualAddonRuntimeContext ctx) {} +} +``` + +`LoVisualAddon.API_VERSION = 0` — сверяется в `AddonManager.java:438`, несовпадение → `WARNING`. + +## Жизнь аддона (`AddonManager.java:39`) + +1. `discoverEntrypoints:211` — `FabricLoader.getEntrypointContainers` → `ADDONS` map, `ENABLED_OVERRIDES` из `AddonStateStore` (вкл/выкл сохраняется). +2. `prepareModuleLoading:58` — `configureEnabledAddons:230` → `onConfigureModules` (до `ModuleAutoLoader`). +3. `init:65` — `reconcileModuleExclusions:255` + `initializeEnabledAddons:272` → `onInitialize`. +4. `notifyClientReady:79` — `onClientReady` после загрузки конфигов. +5. Вкл/выкл из `LoVisualAddonManagerScreen.java:548` `AddonManager.setEnabled:160` — `suspend/resumeRuntimeContributions:363` глушит модули/HUD, пишет `AddonStateStore`. + +Статусы `AddonStatus.java:12`: `DISCOVERED → ACTIVE / DISABLED / ERROR / SUSPENDED`. + +## Что может аддон + +- **Модули** `ctx.registerModule(Class)` — обычный `@ModuleInfo`, попадёт в `ModuleManager`, конфиг `addon:ID:module:ID`. +- **HUD** `registerDraggableHudElement` / `registerStaticHudElement` — `@HudElementInfo` / `@HudElementRegister`, конфиг `addon:ID:hud.*`. +- **Команды** `registerCommand(ClientCommand)` — `@CommandInfo`, префикс `%`. +- **ClickGui секции** `registerClickGuiSection(LoVisualClickGuiSection)`. +- **Рендер** `addRenderCallback(stage, callback)` / `addPostProcessCallback` — `LoVisualRenderStage` (`HUD_RAW`, `WORLD_*`). +- **Расширения модулей** `LoVisualModuleExtension` — `beforeEnable/afterTick` и т.д. через `ModuleExtensionManager`. + +Профили: `AddonManager.moduleProfileOwnerId:129` → `addon:ID:module:ID` для `.lvcfg`. + +## Пример минимального аддона + +См. `api/v0` JavaDoc и `addon/core/AddonManager.java:211`, `addon/pipeline/AddonSnapshot.java:14`, `features/gui/mainmenu/LoVisualAddonManagerScreen.java:48` (UI). + +## Примечание по рефакторингу + +Публичный Java API аддонов **можно ломать свободно** — KISS важнее обратной совместимости (решение владельца 2026-09-05, `TODO.md:9`). При рефакторе меняй сигнатуры, удаляй делегирование, обновляй вызывающих. + diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..40cd782 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,4 @@ +node_modules +dist +dist-ssr +*.local diff --git a/frontend/.oxlintrc.json b/frontend/.oxlintrc.json new file mode 100644 index 0000000..6fa991d --- /dev/null +++ b/frontend/.oxlintrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "typescript", "oxc"], + "rules": { + "react/rules-of-hooks": "error", + "react/only-export-components": ["warn", { "allowConstantExport": true }] + } +} diff --git a/frontend/ARCHITECTURE.md b/frontend/ARCHITECTURE.md new file mode 100644 index 0000000..11ddd04 --- /dev/null +++ b/frontend/ARCHITECTURE.md @@ -0,0 +1,75 @@ +# Frontend Architecture (продумано заранее, реализация — следующий план) + +> Статус: структура согласована до написания implementation-плана фронтенда, +> чтобы план сразу проектировался под правильные границы, а не переписывался +> после первого клубка спагетти. Стек: React + Vite + TypeScript + Tailwind +> (см. `TODO.md`, Фаза 10). Правила платформы (≤250 строк/файл и т.д.) — там же. + +## Принцип: структура по фичам, не по типу файла + +Раскладывать `src/` на `/components`, `/hooks`, `/services`, `/types` вперемешку — +самая частая ошибка в React-проектах: одна фича размазывается по всему дереву +папок, и её нельзя удалить или вынести, не задев остальное. Вместо этого — +каждая фича владеет всем, что ей нужно, в одной папке. + +``` +frontend/src/ + features/ + auth/ # регистрация/логин, device-link экран + components/ + hooks/ + api.ts # вызовы к accounts-service через gateway + types.ts + configs/ # 4 слота, share-коды, витрина + components/ + hooks/ + api.ts + types.ts + avatars/ + admin/ # /admin, видим только role=admin + shared/ # только то, что реально используют 2+ фичи + components/ # кнопки, инпуты, модалки — дизайн-система + hooks/ # useAuth (общий контекст сессии), useDebounce и т.п. + api/ + client.ts # общий fetch-wrapper: base URL gateway, JWT в заголовке, + # обработка 401 (refresh) и 429 (rate-limit) в одном месте + types.ts # общие DTO, если совпадают на нескольких фичах + pages/ # роуты верхнего уровня, тонкие — просто собирают + # фичи в layout, никакой бизнес-логики + App.tsx + main.tsx +``` + +Правило раздела shared/feature: если код используется ровно в одной фиче — +он живёт в этой фиче, а не в `shared/`. Переносить в `shared/` только когда +появился второй потребитель — не заранее "на всякий случай" (YAGNI). + +**Лимиты из правил платформы (`TODO.md`) действуют и тут**: ≤250 строк на +файл, ≤4 файла на папку (подпапки не считаются) — если в `components/` +внутри фичи появляется 5-й файл, выносим смысловую подпапку, а не копим. + +## Антипаттерны, которых сознательно избегаем + +- **Barrel-файлы `index.ts` на каждую папку** — удобны на вид, но ломают + tree-shaking и IDE go-to-definition на больших проектах; экспортируем + напрямую из файла, где определено +- **Prop drilling через 4+ уровня** — если проп передаётся через компоненты, + которым он сам не нужен (просто транзитом) — это сигнал на React Context + (для auth-сессии) или на подъём компонента ниже по дереву, а не терпеть + drilling +- **Бизнес-логика в `pages/`** — страницы только компонуют фичи и layout; + если в файле страницы появляется `fetch`/сложная derived-логика — она + переезжает в соответствующую фичу +- **Один гигантский `api.ts` на весь проект** — обращения к API живут в + папке своей фичи (`features/configs/api.ts` и т.д.), общий только + low-level `shared/api/client.ts` (fetch-wrapper) +- **any вместо нормальных типов** — DTO с бэкенда типизируются по контрактам + из `backend/PLAN.md` (Task 6: `RegisterResponse`, `LoginResponse` и т.д.), + не `any`/`unknown` без сужения + +## Тестирование (когда дойдёт до реализации) +Колокация тестов рядом с кодом (`Component.test.tsx` рядом с `Component.tsx`), +не отдельное дерево `tests/` — так тест физически не потеряется при +удалении/переносе фичи. + +Источники: [Robin Wieruch — React Folder Structure Best Practices 2026](https://www.robinwieruch.de/react-folder-structure/), [Mastering Modern React + Vite Folder Structure, Medium](https://sandeshrathnayake.medium.com/mastering-modern-react-vite-folder-structure-a-production-ready-guide-for-scalable-applications-9ad8e233f8b9). diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..d6af7e3 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,32 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the Oxlint configuration + +If you are developing a production application, we recommend enabling type-aware lint rules by installing `oxlint-tsgolint` and editing `.oxlintrc.json`: + +```json +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "typescript", "oxc"], + "options": { + "typeAware": true + }, + "rules": { + "react/rules-of-hooks": "error", + "react/only-export-components": ["warn", { "allowConstantExport": true }] + } +} +``` + +See the [Oxlint rules documentation](https://oxc.rs/docs/guide/usage/linter/rules) for the full list of rules and categories. diff --git a/frontend/bun.lock b/frontend/bun.lock new file mode 100644 index 0000000..759cb22 --- /dev/null +++ b/frontend/bun.lock @@ -0,0 +1,293 @@ +{ + "lockfileVersion": 2, + "configVersion": 1, + "workspaces": { + "": { + "name": "frontend", + "dependencies": { + "@tailwindcss/vite": "^4.3.3", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "tailwindcss": "^4.3.3", + }, + "devDependencies": { + "@types/node": "^26.6.2", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.7", + "@vitejs/plugin-react": "^6.1.1", + "oxlint": "^1.81.0", + "typescript": "^7.0.2", + "vite": "^8.3.0", + }, + }, + }, + "packages": { + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@oxc-project/types": ["@oxc-project/types@0.151.0", "", {}, "sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA=="], + + "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.85.0", "", { "os": "android", "cpu": "arm" }, "sha512-q2KO/Zso9UT+OMn0NF9ywn4E4t0MI3yxiDhNyhsQ7DyQJrC4FhFE4TXOi4bktFnOWXTMds8qZSbpv2XwRaNOBg=="], + + "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.85.0", "", { "os": "android", "cpu": "arm64" }, "sha512-SxLN3ALjoT9NNdvpjEevGeHvfzTAFrF0NBYB5tzK7/GtCKMze3j1e/m/X2ozqGj2U9hfGG/dg/OG8vpVK4PiDA=="], + + "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.85.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Y/Sup/J4f0f9UGsSd/xyCNTeWL+gepO63GBdEDAfue9nBsnk9zMmnIXx1O6b1V8C90vB5nucYNZ0pbMXAp8zJA=="], + + "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.85.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ApOSNC04ynpDTwvBD+//0wyfODRSbEzvRoKpX8teffmc27z8AockwSNeMXGJXn5KP85eahDgR/2llICWLkzcnw=="], + + "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.85.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-bNrVrCOA/kHky3Tu79IXWXe5bhIgLXfUuUEDHlAGOHUk96MkvDZ1ecaQF19rwstrnaqfP1o9nBTqzIr9+ZHkUg=="], + + "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.85.0", "", { "os": "linux", "cpu": "arm" }, "sha512-NUrzOJ1s/EqsVvfn2L/1D8Wro2LPIZUbihL8kOJLh5fEdGEN3rdOGUYq3HwnUIL8sjpoP+4N6RaGrgmMJnaMPw=="], + + "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.85.0", "", { "os": "linux", "cpu": "arm" }, "sha512-UJXrAT3E/RWkEqXLIs2ehETja1qfgkPb+5gwLIIS+o/6cf+grHvoOXTa5997a/YNQfcJS0DRBTOfZt95cvOI1g=="], + + "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.85.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-lK40QLjI0HxigO7CjDDshEtfYIeiYS0020v5BHFPqN4uuQBQxd2K9LNom2dW15o9F1937quSCRVp4ZsVhdbYdg=="], + + "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.85.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-c2zbdBwGKreHXwRx3gWBuFGJxLhxgsg6YlZ+3H+RgRusU/UEV9jNwJ3HGYK+nRo0LvBa7mt6Kj86xoVotUo8cw=="], + + "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.85.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tlt/Hy8lZ97/lCPmCgw/B3k/mwh+BzaIPbPkldZEly7TwLmx0xe2CQcaW2g/rR0dOgS9JNGCZsMEqLhUNMGvaw=="], + + "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.85.0", "", { "os": "linux", "cpu": "none" }, "sha512-3tNR9Xey82X0zKuY1d8hJ6Rc9gwRDurmqGLnQZa5xqOXy8/YyiqFXjAtugkKLY82obOlpK1eSiDRlgcNPuxtIg=="], + + "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.85.0", "", { "os": "linux", "cpu": "none" }, "sha512-wbGRd5PqCcjkJFHhZuZ2OBSUQY9czlQsoA/cQQB9JK/L9mC5MQgGoKAh+xd8QjA5V+0D3j+Qd1lAWn1I8zlelA=="], + + "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.85.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-3Sn0kSrE4DPZCWV/8o+n4x3aFZxI9ulMnkYlwCbJ8eUVkwRK2IerohE/A/z3SNbCwoPFOCJmGE5Avrq0rrvdvQ=="], + + "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.85.0", "", { "os": "linux", "cpu": "x64" }, "sha512-JY2pxxYfB62bAGfejljVCqc44etItehPuAyaeSAdMuEMtwNA00ggMnS66lC1oIhos6oOXUkuU6mZ9bpFh3BqWg=="], + + "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.85.0", "", { "os": "linux", "cpu": "x64" }, "sha512-5k74vZ6qJBjBHEOlBk9B/iv68Yu0F1Afw/vvT2ar6OGCqEeXLaSjXz2n/IPCbhLG22UoKoYEJTzpYraRdcp6PA=="], + + "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.85.0", "", { "os": "none", "cpu": "arm64" }, "sha512-GbAl5qt5TCkPLXTaIISZJnugrcBhra6rodcXc9jYt620UtdsTt71NlNmJmm0frxzFpd54x/G+MkitEJA8I/BoA=="], + + "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.85.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-kjmws5MK0et2swk4ND85D7NVQyDHw162i6whtZDLUA/lo6FQyBZDcmMRCMcVZcNrAhIaftVb00x9ChGDOjjNJA=="], + + "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.85.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-eSsIJx9n4yxvOqYTZyPEMyEXRmE60XH7xGAU7i0Qbsn1lf6Za3CWJ9aRd82oSFKXaxhp+sA6/yMJVRIpLpna6A=="], + + "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.85.0", "", { "os": "win32", "cpu": "x64" }, "sha512-pBebIPUpKKhWrhSMWhy8TdAZBewiXnfxmaAGxhzxM1068GagqFaTwgKlU6e+UyJ2sPR+VoHouhXuGJkQjsrDvA=="], + + "@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.10", "", { "os": "android", "cpu": "arm" }, "sha512-bp9svZb+QurZeh+8H4BhrZkifEB0YBNvTVzNSJnJQkj4NrRwmQoDUCGP0vSN7PbvLeM7l1tK6GXL8mrTiH2myg=="], + + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.10", "", { "os": "android", "cpu": "arm64" }, "sha512-wm6Dld3RXUAZ/gRWKyUy+4W1B5CB5UeFaOzsSWJWEdxZXHH8rCYiZ5dGe6oJmhsunAPWzL7FZV+VtvmN5Ye2eA=="], + + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UbEfXq/AqGNgRTV3ik+X/iR6mUxu2QdYAadwRxJWquUGnW6gDqdP1FtLtFXRow7RJx0ssRwi80XAPr4r+4DtsA=="], + + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-7f5h17q5KZVx/ji1vb8OTq31ch1O2I7K8NPIr44GkyWTApXMIsmhWqZfgpOH10xeauqghDAvGlZktasCkcF6Eg=="], + + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.10", "", { "os": "freebsd", "cpu": "x64" }, "sha512-ynOk/eEYhC6ZB2xCGvKrEOwE58oBy9LnrAqtkrDF9Fz1VTaNdGZTsV0VarJdhPwb+sOJTGjCLwcuyRJZ1dnMcQ=="], + + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.10", "", { "os": "linux", "cpu": "arm" }, "sha512-ERrAs185meZZhGan7a4l3RiiJK1ArSDlHdST++uvSxe+FDbR4TwUPahT/cbZJvaG6fIpDpF78surN+tX708Y4Q=="], + + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-KN7OHKD0J3jy1UzBwZWPxpwhODf9IARUIJcrH+yLYKOcmegZ8luEUM38lDP1bDVj40yP6PsSzCqOJF76vljFnQ=="], + + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-8l9wP8O+wa8zD6iw6egSfzVtu7oZVfH3hlUsMM4MwbLMhxleqeoXbZzjddyK3YyNlwLhqznq3tF7PkNJ8T/V2w=="], + + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.10", "", { "os": "linux", "cpu": "ppc64" }, "sha512-SeXNKeQzA5kLhz/J0CH6ZP0/HJ3v1xm/0YbiYpE0kK7emfRC2OIGGIaE14xzkISEGv2aYuUSpiLiU5Gbq+OI0A=="], + + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.10", "", { "os": "linux", "cpu": "s390x" }, "sha512-mtht0nR+y8/hart4175Ll15w7lY8dg7CtQ+j2FDNTsDRspOWTK/2V3l0aj9sIj7XmvqxT8Yli/wq22e7feTTWg=="], + + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.10", "", { "os": "linux", "cpu": "x64" }, "sha512-FSM94nGd55NYo48usCyM/nHfUKRnqc9+b0vJNuKV0oCCpIp/OGims7rO1Nv/DkFkt0S/s2rxsJ2kkS8J3HcpeA=="], + + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.10", "", { "os": "linux", "cpu": "x64" }, "sha512-C3YxNB16myRLs7o+B+6PnQ6jBsdIS4+AE4Ah8glVGhDpEv9AOvxhZ/1duAb4B0UGczEK/lBbccksd8VI+p6zfw=="], + + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.10", "", { "os": "none", "cpu": "arm64" }, "sha512-571TlE/F1eeTjjdjYAMMMPs1Mfv3MtX6s3+ZKVU6HiUjZ5Njc6c/qzNy/8K3zALTZnaw3JQVYrHxvNfjm43KAg=="], + + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-QXW+ZWaiqs2c7Fi++D/SsW07LTPcUrncxcskJGfGNBoaLik1IU6fJymz4HsqwEO0u5Iq11yTO0B/mc4cPk7jrQ=="], + + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.10", "", { "os": "win32", "cpu": "x64" }, "sha512-5FQFGgah17YeMtG1Yd5a+rMxQpTksyNXxRtKz06FVTaQw3RKYUJQbUoKk0/5jrXBpDo+7makNP7UHA2LQyH64A=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], + + "@tailwindcss/node": ["@tailwindcss/node@4.3.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.3" } }, "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg=="], + + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.3", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.3", "@tailwindcss/oxide-darwin-arm64": "4.3.3", "@tailwindcss/oxide-darwin-x64": "4.3.3", "@tailwindcss/oxide-freebsd-x64": "4.3.3", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", "@tailwindcss/oxide-linux-x64-musl": "4.3.3", "@tailwindcss/oxide-wasm32-wasi": "4.3.3", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" } }, "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA=="], + + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.3", "", { "os": "android", "cpu": "arm64" }, "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw=="], + + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw=="], + + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw=="], + + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw=="], + + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3", "", { "os": "linux", "cpu": "arm" }, "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ=="], + + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w=="], + + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA=="], + + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w=="], + + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img=="], + + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.3", "", { "dependencies": { "@emnapi/core": "^1.11.1", "@emnapi/runtime": "^1.11.1", "@emnapi/wasi-threads": "^1.2.2", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ=="], + + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ=="], + + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw=="], + + "@tailwindcss/vite": ["@tailwindcss/vite@4.3.3", "", { "dependencies": { "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "tailwindcss": "4.3.3" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw=="], + + "@types/node": ["@types/node@26.6.2", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-X1P21scMv4zGKLYqjdGjaKa7COa0RKVYYZZN/NfvLQ1JegxFhdhpZG/Lyn8AXx6CDUavKAd11v6BvfpkDByK8g=="], + + "@types/react": ["@types/react@19.3.0", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg=="], + + "@types/react-dom": ["@types/react-dom@19.3.0", "", { "peerDependencies": { "@types/react": "^19.3.0" } }, "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q=="], + + "@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="], + + "@typescript/typescript-darwin-arm64": ["@typescript/typescript-darwin-arm64@7.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA=="], + + "@typescript/typescript-darwin-x64": ["@typescript/typescript-darwin-x64@7.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA=="], + + "@typescript/typescript-freebsd-arm64": ["@typescript/typescript-freebsd-arm64@7.0.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ=="], + + "@typescript/typescript-freebsd-x64": ["@typescript/typescript-freebsd-x64@7.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw=="], + + "@typescript/typescript-linux-arm": ["@typescript/typescript-linux-arm@7.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ=="], + + "@typescript/typescript-linux-arm64": ["@typescript/typescript-linux-arm64@7.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ=="], + + "@typescript/typescript-linux-loong64": ["@typescript/typescript-linux-loong64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ=="], + + "@typescript/typescript-linux-mips64el": ["@typescript/typescript-linux-mips64el@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA=="], + + "@typescript/typescript-linux-ppc64": ["@typescript/typescript-linux-ppc64@7.0.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA=="], + + "@typescript/typescript-linux-riscv64": ["@typescript/typescript-linux-riscv64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ=="], + + "@typescript/typescript-linux-s390x": ["@typescript/typescript-linux-s390x@7.0.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw=="], + + "@typescript/typescript-linux-x64": ["@typescript/typescript-linux-x64@7.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A=="], + + "@typescript/typescript-netbsd-arm64": ["@typescript/typescript-netbsd-arm64@7.0.2", "", { "os": "none", "cpu": "arm64" }, "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA=="], + + "@typescript/typescript-netbsd-x64": ["@typescript/typescript-netbsd-x64@7.0.2", "", { "os": "none", "cpu": "x64" }, "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA=="], + + "@typescript/typescript-openbsd-arm64": ["@typescript/typescript-openbsd-arm64@7.0.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ=="], + + "@typescript/typescript-openbsd-x64": ["@typescript/typescript-openbsd-x64@7.0.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg=="], + + "@typescript/typescript-sunos-x64": ["@typescript/typescript-sunos-x64@7.0.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g=="], + + "@typescript/typescript-win32-arm64": ["@typescript/typescript-win32-arm64@7.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ=="], + + "@typescript/typescript-win32-x64": ["@typescript/typescript-win32-x64@7.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g=="], + + "@vitejs/plugin-react": ["@vitejs/plugin-react@6.1.1", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "oxc-transform-react": "^0.145.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler", "oxc-transform-react"] }, "sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "enhanced-resolve": ["enhanced-resolve@5.25.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-nGXts5znJzmWPu+mIE9izCOzdg63oJca2mDzGWWTth7sr4aCToKcoyFVBQwN75Ij5Pf6p510EwkTqViTRzDV+w=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], + + "lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "nanoid": ["nanoid@3.3.19", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug=="], + + "oxlint": ["oxlint@1.85.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.85.0", "@oxlint/binding-android-arm64": "1.85.0", "@oxlint/binding-darwin-arm64": "1.85.0", "@oxlint/binding-darwin-x64": "1.85.0", "@oxlint/binding-freebsd-x64": "1.85.0", "@oxlint/binding-linux-arm-gnueabihf": "1.85.0", "@oxlint/binding-linux-arm-musleabihf": "1.85.0", "@oxlint/binding-linux-arm64-gnu": "1.85.0", "@oxlint/binding-linux-arm64-musl": "1.85.0", "@oxlint/binding-linux-ppc64-gnu": "1.85.0", "@oxlint/binding-linux-riscv64-gnu": "1.85.0", "@oxlint/binding-linux-riscv64-musl": "1.85.0", "@oxlint/binding-linux-s390x-gnu": "1.85.0", "@oxlint/binding-linux-x64-gnu": "1.85.0", "@oxlint/binding-linux-x64-musl": "1.85.0", "@oxlint/binding-openharmony-arm64": "1.85.0", "@oxlint/binding-win32-arm64-msvc": "1.85.0", "@oxlint/binding-win32-ia32-msvc": "1.85.0", "@oxlint/binding-win32-x64-msvc": "1.85.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2001", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-bc26s97nuvPj1ViyPsqmKecVkUWFMEdtayO8MaQ6oiLfs1pj94cQlZZhrh4BPNlr9HQosjhIlwgZKsfcwmcNgg=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], + + "postcss": ["postcss@8.5.28", "", { "dependencies": { "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A=="], + + "react": ["react@19.3.0", "", {}, "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog=="], + + "react-dom": ["react-dom@19.3.0", "", { "dependencies": { "scheduler": "^0.28.0" }, "peerDependencies": { "react": "^19.3.0" } }, "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q=="], + + "rolldown": ["rolldown@1.2.10", "", { "dependencies": { "@oxc-project/types": "=0.151.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.10", "@rolldown/binding-android-arm64": "1.2.10", "@rolldown/binding-darwin-arm64": "1.2.10", "@rolldown/binding-darwin-x64": "1.2.10", "@rolldown/binding-freebsd-x64": "1.2.10", "@rolldown/binding-linux-arm-gnueabihf": "1.2.10", "@rolldown/binding-linux-arm64-gnu": "1.2.10", "@rolldown/binding-linux-arm64-musl": "1.2.10", "@rolldown/binding-linux-ppc64-gnu": "1.2.10", "@rolldown/binding-linux-s390x-gnu": "1.2.10", "@rolldown/binding-linux-x64-gnu": "1.2.10", "@rolldown/binding-linux-x64-musl": "1.2.10", "@rolldown/binding-openharmony-arm64": "1.2.10", "@rolldown/binding-win32-arm64-msvc": "1.2.10", "@rolldown/binding-win32-x64-msvc": "1.2.10" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-OxkA08pSryMK7B3XiFA09B4OJ1xJMPgIYCBMY2xchzpqgBGsV1o0DetPAE+Sl3N3L4oCPiEzmHVSOj7iR04Zog=="], + + "scheduler": ["scheduler@0.28.0", "", {}, "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="], + + "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], + + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + + "typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], + + "undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="], + + "vite": ["vite@8.3.0", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", "postcss": "^8.5.28", "rolldown": "~1.2.6", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.7.1", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ=="], + + "@tailwindcss/node/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g=="], + + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" }, "bundled": true }, "sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g=="], + + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.4", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A=="], + + "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "@tailwindcss/node/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + + "@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "@tailwindcss/node/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "@tailwindcss/node/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "@tailwindcss/node/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "@tailwindcss/node/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + } +} diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..0fca6f0 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + frontend + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..3ff622e --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,27 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "oxlint", + "preview": "vite preview" + }, + "dependencies": { + "@tailwindcss/vite": "^4.3.3", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "tailwindcss": "^4.3.3" + }, + "devDependencies": { + "@types/node": "^26.6.2", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.7", + "@vitejs/plugin-react": "^6.1.1", + "oxlint": "^1.81.0", + "typescript": "^7.0.2", + "vite": "^8.3.0" + } +} diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/icons.svg b/frontend/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/frontend/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000..f90339d --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,184 @@ +.counter { + font-size: 16px; + padding: 5px 10px; + border-radius: 5px; + color: var(--accent); + background: var(--accent-bg); + border: 2px solid transparent; + transition: border-color 0.3s; + margin-bottom: 24px; + + &:hover { + border-color: var(--accent-border); + } + &:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + } +} + +.hero { + position: relative; + + .base, + .framework, + .vite { + inset-inline: 0; + margin: 0 auto; + } + + .base { + width: 170px; + position: relative; + z-index: 0; + } + + .framework, + .vite { + position: absolute; + } + + .framework { + z-index: 1; + top: 34px; + height: 28px; + transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) + scale(1.4); + } + + .vite { + z-index: 0; + top: 107px; + height: 26px; + width: auto; + transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) + scale(0.8); + } +} + +#center { + display: flex; + flex-direction: column; + gap: 25px; + place-content: center; + place-items: center; + flex-grow: 1; + + @media (max-width: 1024px) { + padding: 32px 20px 24px; + gap: 18px; + } +} + +#next-steps { + display: flex; + border-top: 1px solid var(--border); + text-align: left; + + & > div { + flex: 1 1 0; + padding: 32px; + @media (max-width: 1024px) { + padding: 24px 20px; + } + } + + .icon { + margin-bottom: 16px; + width: 22px; + height: 22px; + } + + @media (max-width: 1024px) { + flex-direction: column; + text-align: center; + } +} + +#docs { + border-right: 1px solid var(--border); + + @media (max-width: 1024px) { + border-right: none; + border-bottom: 1px solid var(--border); + } +} + +#next-steps ul { + list-style: none; + padding: 0; + display: flex; + gap: 8px; + margin: 32px 0 0; + + .logo { + height: 18px; + } + + a { + color: var(--text-h); + font-size: 16px; + border-radius: 6px; + background: var(--social-bg); + display: flex; + padding: 6px 12px; + align-items: center; + gap: 8px; + text-decoration: none; + transition: box-shadow 0.3s; + + &:hover { + box-shadow: var(--shadow); + } + .button-icon { + height: 18px; + width: 18px; + } + } + + @media (max-width: 1024px) { + margin-top: 20px; + flex-wrap: wrap; + justify-content: center; + + li { + flex: 1 1 calc(50% - 8px); + } + + a { + width: 100%; + justify-content: center; + box-sizing: border-box; + } + } +} + +#spacer { + height: 88px; + border-top: 1px solid var(--border); + @media (max-width: 1024px) { + height: 48px; + } +} + +.ticks { + position: relative; + width: 100%; + + &::before, + &::after { + content: ''; + position: absolute; + top: -4.5px; + border: 5px solid transparent; + } + + &::before { + left: 0; + border-left-color: var(--border); + } + &::after { + right: 0; + border-right-color: var(--border); + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..9ca8e8d --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,122 @@ +import { useState } from 'react' +import heroImg from './assets/hero.png' +import reactLogo from './assets/react.svg' +import viteLogo from './assets/vite.svg' +import './App.css' + +function App() { + const [count, setCount] = useState(0) + + return ( + <> +
+
+ + React logo + Vite logo +
+
+

Get started

+

+ Edit src/App.tsx and save to test HMR +

+
+ +
+ +
+ +
+
+ +

Documentation

+

Your questions, answered

+ +
+
+ +

Connect with us

+

Join the Vite community

+ +
+
+ +
+
+ + ) +} + +export default App diff --git a/frontend/src/assets/hero.png b/frontend/src/assets/hero.png new file mode 100644 index 0000000..02251f4 Binary files /dev/null and b/frontend/src/assets/hero.png differ diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/vite.svg b/frontend/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/frontend/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..f1d8c73 --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..bef5202 --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 0000000..6830b6f --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023", "DOM"], + "module": "esnext", + "types": ["vite/client"], + "allowArbitraryExtensions": true, + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..8455dcb --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "module": "nodenext", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..9f9b446 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,8 @@ +import tailwindcss from '@tailwindcss/vite' +import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react(), tailwindcss()], +}) diff --git a/combatant-client-26.2/.gitignore b/mod/.gitignore similarity index 100% rename from combatant-client-26.2/.gitignore rename to mod/.gitignore diff --git a/combatant-client-26.2/DEV_GUIDE.md b/mod/DEV_GUIDE.md similarity index 100% rename from combatant-client-26.2/DEV_GUIDE.md rename to mod/DEV_GUIDE.md diff --git a/combatant-client-26.2/LICENSE b/mod/LICENSE similarity index 100% rename from combatant-client-26.2/LICENSE rename to mod/LICENSE diff --git a/combatant-client-26.2/README.md b/mod/README.md similarity index 100% rename from combatant-client-26.2/README.md rename to mod/README.md diff --git a/combatant-client-26.2/TODO.md b/mod/TODO.md similarity index 92% rename from combatant-client-26.2/TODO.md rename to mod/TODO.md index 9aac544..8ae521c 100644 --- a/combatant-client-26.2/TODO.md +++ b/mod/TODO.md @@ -9,6 +9,8 @@ - **Публичный API можно менять свободно** (решение владельца 2026-09-05): KISS важнее обратной совместимости; при рефакторинге разрешено менять сигнатуры, удалять делегирование и мёртвый код, обновляя вызывающих +- **Коммиты — на английском** (решение 2026-09-23, действует с этого момента + вперёд; более старые коммиты на русском не переписываем) - **Никакого статика** — `Minecraft.getInstance()` только лениво, никогда в поле - **Никаких this-escape** — конструктор не публикует `this` - **Никаких мёртвых миксинов** — миксин только если реально нужен; `cancellable=true` только при реальном cancel @@ -303,27 +305,29 @@ - [x] UiPrimitiveRenderer 1053 → ГОТОВО (2026-09-10): фасад 188 (коннекторы) + render/primitive/ 4 (Context 153, Props 158, Painter 145, GlowPainter 147) + render/rect/ 2 (RectPainter 150, CornerPainter 150), build+test ✓ -- [ ] BetterButtons 1032 — extraction+tooltip+render → `hud/nondraggable/impl/buttons/` -- [ ] InventorySwap 1031 — по стратегиям свапа; чистая логика отдельными классами с тестами -- [ ] SvgMeshBackend 1014 — parser/triangulate/cache разнести -- [ ] Themes 1012 — реестр тем в `theme/presets/` (по 4 темы/файл), дефолты отдельно -- [ ] WorldParticles 991 — типы частиц в `visuals/effects/particles/` -- [ ] Module 989 — базовый класс: settings-механику вынести в `module/ext/` (осторожно, общий предок!) -- [ ] LoVisualAddonManagerScreen 989 — как AltManager -- [ ] Chams 976 — режимы рендера в подклассы-политики (фасад модуля тонкий) -- [ ] ClickGuiPickerState 962 + CombatProtocolHeuristicsEditorState 932 — state/actions хелперы -- [ ] ModulesMenuScreen 960 — рендер категорий отдельно от состояния -- [ ] UiMeshGeometry 954 — по примитивам геометрии -- [ ] BetterTooltips 953, OrderedUiBatcher 935 (→ batches + blur), Scoreboard 924, - Rain 893 (+CompactHudStatModel 893), TazikHat 859 ✓ закрыт портированием в +- [x] BetterButtons 1032 → ГОТОВО 7e86f8d: 388 + 7×≤200 (state/hover/parallax/queue/render) — extraction+tooltip+render → `hud/nondraggable/impl/buttons/` +- [x] InventorySwap 1031 → УДАЛЁН 5acafc8: чит-инфра, заменён ShitEsp + хотбар-only — по стратегиям свапа; чистая логика отдельными классами с тестами +- [x] SvgMeshBackend 1014 → ГОТОВО c11ebe3/aafe7de: 1020→267 + Parser181/Geometry173/Texture223/Render35/Cache25 — parser/triangulate/cache разнести +- [x] Themes 1012 → ГОТОВО e51a610: 1012→562 + 3×163 пресета — реестр тем в `theme/presets/` (по 4 темы/файл), дефолты отдельно +- [x] WorldParticles 991 → ГОТОВО 1d757a6+5a463d7e: 999→821→260 (фасад) + ambient/render/ 3×≤182 + (Batches/Bubble/Funnel) + ambient/particle/ 197 + geometry/ доработаны (offset/multiplyAlpha/STACKS) — + типы частиц в `visuals/effects/ambient/` +- [x] Module 989 → ГОТОВО c8da336: 1031→139 + 4×≤134 (ext/) — базовый класс: settings-механику вынести в `module/ext/` (осторожно, общий предок!) +- [ ] LoVisualAddonManagerScreen 989 — как AltManager — как AltManager +- [x] Chams 976 → ГОТОВО 78b5335: 985→129 + 4×≤195 — режимы рендера в подклассы-политики (фасад модуля тонкий) +- [x] ClickGuiPickerState 962 → ГОТОВО 234f0d9: 970→166 + 4×≤140; CombatProtocolHeuristicsEditorState 932→79 +4×≤192 03ae4a6 — state/actions хелперы +- [x] ModulesMenuScreen 960 → ГОТОВО 234f0d9: 966→145 + 4×≤178 — рендер категорий отдельно от состояния +- [x] UiMeshGeometry 954 → ГОТОВО sub-agent: 956→99 + 4×≤135 — по примитивам геометрии +- [x] BetterTooltips 953 → ГОТОВО 0b8ea41: 960→181 +4×≤199; OrderedUiBatcher 935 → ГОТОВО 6e0bf7f: 944→188 (→ batches + blur), Scoreboard 924, + Rain 893 → ГОТОВО sub-agent: 903→159 +4×≤162; CompactHudStatModel 893 → ГОТОВО 8979140: 894→125 +4×≤175; TazikHat 859 ✓ ✓ закрыт портированием в Cosmetics built-in (2026-09-11, см. Фазу 7), - ShaderPatchCompiler 843, OnlineRelationPlayerPickerComponent 837, CustomTabList 830, - GameRendererMixin 810 (только хуки; тело вынести в `render/engine/entry/`-классы — - ВНИМАНИЕ: 9.1 тоже его трогает, координировать), Predictions 805 -- [ ] Хвост 400–800 (~78 файлов, см. /tmp-снимок в истории команд: ConfigProfilesComponent 791, + ShaderPatchCompiler 843 → ГОТОВО 2b55fbc: 843→66+4×≤174; OnlineRelationPlayerPickerComponent 837→74+4×≤192 17fc665; CustomTabList 830 → ГОТОВО 9e9b71d: 834→136+4×≤166, + GameRendererMixin 830 → ГОТОВО f45f46e: 830→344 (только хуки+Host/BobInvoker) + + mixins/render/main/handlers/ 4×≤200 (ProjectionHelper/WorldEngine/WorldPost/WorldDebug), Predictions 805 → ГОТОВО (см. хвост) +- [x] Хвост 400–800: ЧАСТИЧНО — ConfigProfilesComponent 799→125 d95bfec, HitEffect 770→120 5e1ce05, PlayerMovementSimulation 757→157 4b253d1, ProjectilePuncher 747→104 fba4f7d, MediaPlayer 739→174 8ce3927, Crosshair 737→143 4cf0970, Itemizer 699→193/Cooldowns 707→151 11245ac, Potions 617→176 2e4912a, Admins 702→118 f18cb96, Keybinds 611→157 cc18feb, ModuleList 499→181 52327dc, Radar 392→161 f580373, Fps 309→168 5df2cc0, Memory 304→151 de7cc2b, Tps 339→154 e167f0c, Ping 309→145 15d04cb, SpeedBps 334→156 b78a638, GameTime 314→150 922d3bc, SystemTime 300→136 3cb6e40, Coordinates 337→174 9412982, Armor 324→162 932639d, Inventory 476→153 f4124f4, BlockHighlight 692→271+4×≤200 9f76e5a5 (scene/highlight/), см. /tmp-снимок в истории команд: ConfigProfilesComponent 791, HitEffect 770, PlayerMovementSimulation 755, ProjectilePuncher 746, MediaPlayer 739, Crosshair 737, MeshBuilder 729, SettingsPanelComponent 703, Admins 702, Cooldowns 701, - Itemizer 699, BlockHighlight 692, LocalPlayerMixin 687, Trails 681, ItemBatchRenderer 673, + Itemizer 699, LocalPlayerMixin 687, Trails 681, ItemBatchRenderer 673, CosmeticModel 671, MenuScreen 667, GlStencilFramebufferSupport 663, ConfigSerializer 663, DynamicIsland 661, ReimaginedVisual 660, RelationsComponent 659, UiStyle 640, Potions 617, ModuleComponent 612, MsdfFont 606, Keybinds 603, MediaSessionService 594 и др.) diff --git a/combatant-client-26.2/build.gradle b/mod/build.gradle similarity index 100% rename from combatant-client-26.2/build.gradle rename to mod/build.gradle diff --git a/combatant-client-26.2/config/checkstyle/checkstyle.xml b/mod/config/checkstyle/checkstyle.xml similarity index 100% rename from combatant-client-26.2/config/checkstyle/checkstyle.xml rename to mod/config/checkstyle/checkstyle.xml diff --git a/combatant-client-26.2/config/lovisual/hud/draggable/admins.json b/mod/config/lovisual/hud/draggable/admins.json similarity index 100% rename from combatant-client-26.2/config/lovisual/hud/draggable/admins.json rename to mod/config/lovisual/hud/draggable/admins.json diff --git a/combatant-client-26.2/config/lovisual/hud/draggable/armor.json b/mod/config/lovisual/hud/draggable/armor.json similarity index 100% rename from combatant-client-26.2/config/lovisual/hud/draggable/armor.json rename to mod/config/lovisual/hud/draggable/armor.json diff --git a/combatant-client-26.2/config/lovisual/hud/draggable/better_chat.json b/mod/config/lovisual/hud/draggable/better_chat.json similarity index 100% rename from combatant-client-26.2/config/lovisual/hud/draggable/better_chat.json rename to mod/config/lovisual/hud/draggable/better_chat.json diff --git a/combatant-client-26.2/config/lovisual/hud/draggable/combat_log.json b/mod/config/lovisual/hud/draggable/combat_log.json similarity index 100% rename from combatant-client-26.2/config/lovisual/hud/draggable/combat_log.json rename to mod/config/lovisual/hud/draggable/combat_log.json diff --git a/combatant-client-26.2/config/lovisual/hud/draggable/fps.json b/mod/config/lovisual/hud/draggable/fps.json similarity index 100% rename from combatant-client-26.2/config/lovisual/hud/draggable/fps.json rename to mod/config/lovisual/hud/draggable/fps.json diff --git a/combatant-client-26.2/config/lovisual/hud/draggable/game_time.json b/mod/config/lovisual/hud/draggable/game_time.json similarity index 100% rename from combatant-client-26.2/config/lovisual/hud/draggable/game_time.json rename to mod/config/lovisual/hud/draggable/game_time.json diff --git a/combatant-client-26.2/config/lovisual/hud/draggable/hud_notifier.json b/mod/config/lovisual/hud/draggable/hud_notifier.json similarity index 100% rename from combatant-client-26.2/config/lovisual/hud/draggable/hud_notifier.json rename to mod/config/lovisual/hud/draggable/hud_notifier.json diff --git a/combatant-client-26.2/config/lovisual/hud/draggable/inventory.json b/mod/config/lovisual/hud/draggable/inventory.json similarity index 100% rename from combatant-client-26.2/config/lovisual/hud/draggable/inventory.json rename to mod/config/lovisual/hud/draggable/inventory.json diff --git a/combatant-client-26.2/config/lovisual/hud/draggable/itemizer.json b/mod/config/lovisual/hud/draggable/itemizer.json similarity index 100% rename from combatant-client-26.2/config/lovisual/hud/draggable/itemizer.json rename to mod/config/lovisual/hud/draggable/itemizer.json diff --git a/combatant-client-26.2/config/lovisual/hud/draggable/keybinds.json b/mod/config/lovisual/hud/draggable/keybinds.json similarity index 100% rename from combatant-client-26.2/config/lovisual/hud/draggable/keybinds.json rename to mod/config/lovisual/hud/draggable/keybinds.json diff --git a/combatant-client-26.2/config/lovisual/hud/draggable/media_player.json b/mod/config/lovisual/hud/draggable/media_player.json similarity index 100% rename from combatant-client-26.2/config/lovisual/hud/draggable/media_player.json rename to mod/config/lovisual/hud/draggable/media_player.json diff --git a/combatant-client-26.2/config/lovisual/hud/draggable/memory.json b/mod/config/lovisual/hud/draggable/memory.json similarity index 100% rename from combatant-client-26.2/config/lovisual/hud/draggable/memory.json rename to mod/config/lovisual/hud/draggable/memory.json diff --git a/combatant-client-26.2/config/lovisual/hud/draggable/module_list.json b/mod/config/lovisual/hud/draggable/module_list.json similarity index 100% rename from combatant-client-26.2/config/lovisual/hud/draggable/module_list.json rename to mod/config/lovisual/hud/draggable/module_list.json diff --git a/combatant-client-26.2/config/lovisual/hud/draggable/ping.json b/mod/config/lovisual/hud/draggable/ping.json similarity index 100% rename from combatant-client-26.2/config/lovisual/hud/draggable/ping.json rename to mod/config/lovisual/hud/draggable/ping.json diff --git a/combatant-client-26.2/config/lovisual/hud/draggable/potions.json b/mod/config/lovisual/hud/draggable/potions.json similarity index 100% rename from combatant-client-26.2/config/lovisual/hud/draggable/potions.json rename to mod/config/lovisual/hud/draggable/potions.json diff --git a/combatant-client-26.2/config/lovisual/hud/draggable/radar.json b/mod/config/lovisual/hud/draggable/radar.json similarity index 100% rename from combatant-client-26.2/config/lovisual/hud/draggable/radar.json rename to mod/config/lovisual/hud/draggable/radar.json diff --git a/combatant-client-26.2/config/lovisual/hud/draggable/scoreboard.json b/mod/config/lovisual/hud/draggable/scoreboard.json similarity index 100% rename from combatant-client-26.2/config/lovisual/hud/draggable/scoreboard.json rename to mod/config/lovisual/hud/draggable/scoreboard.json diff --git a/combatant-client-26.2/config/lovisual/hud/draggable/speed_bps.json b/mod/config/lovisual/hud/draggable/speed_bps.json similarity index 100% rename from combatant-client-26.2/config/lovisual/hud/draggable/speed_bps.json rename to mod/config/lovisual/hud/draggable/speed_bps.json diff --git a/combatant-client-26.2/config/lovisual/hud/draggable/system_time.json b/mod/config/lovisual/hud/draggable/system_time.json similarity index 100% rename from combatant-client-26.2/config/lovisual/hud/draggable/system_time.json rename to mod/config/lovisual/hud/draggable/system_time.json diff --git a/combatant-client-26.2/config/lovisual/hud/draggable/target_hud.json b/mod/config/lovisual/hud/draggable/target_hud.json similarity index 100% rename from combatant-client-26.2/config/lovisual/hud/draggable/target_hud.json rename to mod/config/lovisual/hud/draggable/target_hud.json diff --git a/combatant-client-26.2/config/lovisual/hud/draggable/timer_indicator.json b/mod/config/lovisual/hud/draggable/timer_indicator.json similarity index 100% rename from combatant-client-26.2/config/lovisual/hud/draggable/timer_indicator.json rename to mod/config/lovisual/hud/draggable/timer_indicator.json diff --git a/combatant-client-26.2/config/lovisual/hud/draggable/tps.json b/mod/config/lovisual/hud/draggable/tps.json similarity index 100% rename from combatant-client-26.2/config/lovisual/hud/draggable/tps.json rename to mod/config/lovisual/hud/draggable/tps.json diff --git a/combatant-client-26.2/config/lovisual/hud/draggable/triangulator.json b/mod/config/lovisual/hud/draggable/triangulator.json similarity index 100% rename from combatant-client-26.2/config/lovisual/hud/draggable/triangulator.json rename to mod/config/lovisual/hud/draggable/triangulator.json diff --git a/combatant-client-26.2/config/lovisual/hud/draggable/watermark.json b/mod/config/lovisual/hud/draggable/watermark.json similarity index 100% rename from combatant-client-26.2/config/lovisual/hud/draggable/watermark.json rename to mod/config/lovisual/hud/draggable/watermark.json diff --git a/combatant-client-26.2/config/lovisual/hud/draggable/xyz.json b/mod/config/lovisual/hud/draggable/xyz.json similarity index 100% rename from combatant-client-26.2/config/lovisual/hud/draggable/xyz.json rename to mod/config/lovisual/hud/draggable/xyz.json diff --git a/combatant-client-26.2/config/lovisual/hud/nondraggable/hud_element_dynamic_island.json b/mod/config/lovisual/hud/nondraggable/hud_element_dynamic_island.json similarity index 100% rename from combatant-client-26.2/config/lovisual/hud/nondraggable/hud_element_dynamic_island.json rename to mod/config/lovisual/hud/nondraggable/hud_element_dynamic_island.json diff --git a/combatant-client-26.2/config/lovisual/hud/nondraggable/hud_element_reach_display.json b/mod/config/lovisual/hud/nondraggable/hud_element_reach_display.json similarity index 100% rename from combatant-client-26.2/config/lovisual/hud/nondraggable/hud_element_reach_display.json rename to mod/config/lovisual/hud/nondraggable/hud_element_reach_display.json diff --git a/combatant-client-26.2/config/lovisual/hud/nondraggable/hud_element_swap_tooltip.json b/mod/config/lovisual/hud/nondraggable/hud_element_swap_tooltip.json similarity index 100% rename from combatant-client-26.2/config/lovisual/hud/nondraggable/hud_element_swap_tooltip.json rename to mod/config/lovisual/hud/nondraggable/hud_element_swap_tooltip.json diff --git a/combatant-client-26.2/config/lovisual/hud/nondraggable/hud_element_tab_list.json b/mod/config/lovisual/hud/nondraggable/hud_element_tab_list.json similarity index 100% rename from combatant-client-26.2/config/lovisual/hud/nondraggable/hud_element_tab_list.json rename to mod/config/lovisual/hud/nondraggable/hud_element_tab_list.json diff --git a/combatant-client-26.2/config/lovisual/hud/nondraggable/hud_element_vanilla_bar.json b/mod/config/lovisual/hud/nondraggable/hud_element_vanilla_bar.json similarity index 100% rename from combatant-client-26.2/config/lovisual/hud/nondraggable/hud_element_vanilla_bar.json rename to mod/config/lovisual/hud/nondraggable/hud_element_vanilla_bar.json diff --git a/combatant-client-26.2/config/lovisual/hud/nondraggable/hud_element_vanilla_buttons.json b/mod/config/lovisual/hud/nondraggable/hud_element_vanilla_buttons.json similarity index 100% rename from combatant-client-26.2/config/lovisual/hud/nondraggable/hud_element_vanilla_buttons.json rename to mod/config/lovisual/hud/nondraggable/hud_element_vanilla_buttons.json diff --git a/combatant-client-26.2/config/lovisual/hud/nondraggable/hud_element_vanilla_health.json b/mod/config/lovisual/hud/nondraggable/hud_element_vanilla_health.json similarity index 100% rename from combatant-client-26.2/config/lovisual/hud/nondraggable/hud_element_vanilla_health.json rename to mod/config/lovisual/hud/nondraggable/hud_element_vanilla_health.json diff --git a/combatant-client-26.2/config/lovisual/hud/nondraggable/hud_element_vanilla_hotbar.json b/mod/config/lovisual/hud/nondraggable/hud_element_vanilla_hotbar.json similarity index 100% rename from combatant-client-26.2/config/lovisual/hud/nondraggable/hud_element_vanilla_hotbar.json rename to mod/config/lovisual/hud/nondraggable/hud_element_vanilla_hotbar.json diff --git a/combatant-client-26.2/config/lovisual/hud/nondraggable/hud_element_vanilla_tooltips.json b/mod/config/lovisual/hud/nondraggable/hud_element_vanilla_tooltips.json similarity index 100% rename from combatant-client-26.2/config/lovisual/hud/nondraggable/hud_element_vanilla_tooltips.json rename to mod/config/lovisual/hud/nondraggable/hud_element_vanilla_tooltips.json diff --git a/combatant-client-26.2/config/lovisual/hud/nondraggable/hud_element_watermark.json b/mod/config/lovisual/hud/nondraggable/hud_element_watermark.json similarity index 100% rename from combatant-client-26.2/config/lovisual/hud/nondraggable/hud_element_watermark.json rename to mod/config/lovisual/hud/nondraggable/hud_element_watermark.json diff --git a/combatant-client-26.2/config/lovisual/mainconfig.json5 b/mod/config/lovisual/mainconfig.json5 similarity index 100% rename from combatant-client-26.2/config/lovisual/mainconfig.json5 rename to mod/config/lovisual/mainconfig.json5 diff --git a/combatant-client-26.2/config/pmd/ruleset.xml b/mod/config/pmd/ruleset.xml similarity index 100% rename from combatant-client-26.2/config/pmd/ruleset.xml rename to mod/config/pmd/ruleset.xml diff --git a/mod/docs/README.md b/mod/docs/README.md new file mode 100644 index 0000000..ef92243 --- /dev/null +++ b/mod/docs/README.md @@ -0,0 +1,5 @@ +# docs + +- `DEV_GUIDE.md` — единый справочник разработчика (сборка, правила ≤200/≤4, пакеты, модули/HUD/команды/миксины/рендер/тесты). Объединён из `DEV_GUIDE.md`+`DEVELOPMENT.md` 2026-09-23. +- `api/addons.md` — API аддонов (`lovisual:addon`, жизненный цикл, что может аддон). Публичный API можно ломать свободно при рефакторинге (KISS > совместимость). +- `TODO.md` в корне — план фаз (сейчас 8.5/9.1/9.2/10/11). diff --git a/mod/docs/api/addons.md b/mod/docs/api/addons.md new file mode 100644 index 0000000..db4c63c --- /dev/null +++ b/mod/docs/api/addons.md @@ -0,0 +1,73 @@ +# LoVisual Addon API (`api/v0`) + +Аддоны — обычные Fabric-моды, которые расширяют LoVisual. Точка входа `lovisual:addon` в `fabric.mod.json`. Можно свободно менять публичный API при рефакторинге — владелец разрешил ломать совместимость ради KISS. + +## Entrypoint + +`fabric.mod.json`: +```json +{ + "schemaVersion": 1, + "id": "my-addon", + "name": "My Addon", + "entrypoints": { + "lovisual:addon": ["com.example.MyAddon"] + }, + "custom": { + "lovisual:addon": { "api": 0, "icon": "assets/my-addon/icon.png" } + } +} +``` + +Класс: +```java +public class MyAddon implements LoVisualAddon { + public void onConfigureModules(LoVisualModuleLoadContext ctx) { + ctx.disableBuiltInModule("killAura"); // выключить встроенный модуль до его создания + } + public void onInitialize(LoVisualAddonContext ctx) { + ctx.registerModule(MyModule.class); + ctx.registerDraggableHudElement(MyHud.class); + ctx.registerStaticHudElement(MyStaticHud.class); + ctx.registerCommand(new MyCommand()); + ctx.registerClickGuiSection(new MySection()); + ctx.addRenderCallback(LoVisualRenderStage.HUD_RAW, (c) -> { /* 2D */ }); + } + public void onClientReady(LoVisualAddonRuntimeContext ctx) {} + public void onShutdown(LoVisualAddonRuntimeContext ctx) {} + public void onRuntimeSuspended(LoVisualAddonRuntimeContext ctx) {} // при выкл. аддона + public void onRuntimeResumed(LoVisualAddonRuntimeContext ctx) {} +} +``` + +`LoVisualAddon.API_VERSION = 0` — сверяется в `AddonManager.java:438`, несовпадение → `WARNING`. + +## Жизнь аддона (`AddonManager.java:39`) + +1. `discoverEntrypoints:211` — `FabricLoader.getEntrypointContainers` → `ADDONS` map, `ENABLED_OVERRIDES` из `AddonStateStore` (вкл/выкл сохраняется). +2. `prepareModuleLoading:58` — `configureEnabledAddons:230` → `onConfigureModules` (до `ModuleAutoLoader`). +3. `init:65` — `reconcileModuleExclusions:255` + `initializeEnabledAddons:272` → `onInitialize`. +4. `notifyClientReady:79` — `onClientReady` после загрузки конфигов. +5. Вкл/выкл из `LoVisualAddonManagerScreen.java:548` `AddonManager.setEnabled:160` — `suspend/resumeRuntimeContributions:363` глушит модули/HUD, пишет `AddonStateStore`. + +Статусы `AddonStatus.java:12`: `DISCOVERED → ACTIVE / DISABLED / ERROR / SUSPENDED`. + +## Что может аддон + +- **Модули** `ctx.registerModule(Class)` — обычный `@ModuleInfo`, попадёт в `ModuleManager`, конфиг `addon:ID:module:ID`. +- **HUD** `registerDraggableHudElement` / `registerStaticHudElement` — `@HudElementInfo` / `@HudElementRegister`, конфиг `addon:ID:hud.*`. +- **Команды** `registerCommand(ClientCommand)` — `@CommandInfo`, префикс `%`. +- **ClickGui секции** `registerClickGuiSection(LoVisualClickGuiSection)`. +- **Рендер** `addRenderCallback(stage, callback)` / `addPostProcessCallback` — `LoVisualRenderStage` (`HUD_RAW`, `WORLD_*`). +- **Расширения модулей** `LoVisualModuleExtension` — `beforeEnable/afterTick` и т.д. через `ModuleExtensionManager`. + +Профили: `AddonManager.moduleProfileOwnerId:129` → `addon:ID:module:ID` для `.lvcfg`. + +## Пример минимального аддона + +См. `api/v0` JavaDoc и `addon/core/AddonManager.java:211`, `addon/pipeline/AddonSnapshot.java:14`, `features/gui/mainmenu/LoVisualAddonManagerScreen.java:48` (UI). + +## Примечание по рефакторингу + +Публичный Java API аддонов **можно ломать свободно** — KISS важнее обратной совместимости (решение владельца 2026-09-05, `TODO.md:9`). При рефакторе меняй сигнатуры, удаляй делегирование, обновляй вызывающих. + diff --git a/combatant-client-26.2/gradle.properties b/mod/gradle.properties similarity index 100% rename from combatant-client-26.2/gradle.properties rename to mod/gradle.properties diff --git a/combatant-client-26.2/gradle/wrapper/gradle-wrapper.jar b/mod/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from combatant-client-26.2/gradle/wrapper/gradle-wrapper.jar rename to mod/gradle/wrapper/gradle-wrapper.jar diff --git a/combatant-client-26.2/gradle/wrapper/gradle-wrapper.properties b/mod/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from combatant-client-26.2/gradle/wrapper/gradle-wrapper.properties rename to mod/gradle/wrapper/gradle-wrapper.properties diff --git a/combatant-client-26.2/lombok.config b/mod/lombok.config similarity index 100% rename from combatant-client-26.2/lombok.config rename to mod/lombok.config diff --git a/combatant-client-26.2/settings.gradle b/mod/settings.gradle similarity index 100% rename from combatant-client-26.2/settings.gradle rename to mod/settings.gradle diff --git a/combatant-client-26.2/src/dev/java/dev/loki/lovisual/features/command/impl/ProfilerCommand.java b/mod/src/dev/java/dev/loki/lovisual/features/command/impl/ProfilerCommand.java similarity index 100% rename from combatant-client-26.2/src/dev/java/dev/loki/lovisual/features/command/impl/ProfilerCommand.java rename to mod/src/dev/java/dev/loki/lovisual/features/command/impl/ProfilerCommand.java diff --git a/combatant-client-26.2/src/dev/java/dev/loki/lovisual/mixins/ProfilersMixin.java b/mod/src/dev/java/dev/loki/lovisual/mixins/ProfilersMixin.java similarity index 100% rename from combatant-client-26.2/src/dev/java/dev/loki/lovisual/mixins/ProfilersMixin.java rename to mod/src/dev/java/dev/loki/lovisual/mixins/ProfilersMixin.java diff --git a/combatant-client-26.2/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevFrameStutterProfiler.java b/mod/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevFrameStutterProfiler.java similarity index 100% rename from combatant-client-26.2/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevFrameStutterProfiler.java rename to mod/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevFrameStutterProfiler.java diff --git a/combatant-client-26.2/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevGlSyncTracker.java b/mod/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevGlSyncTracker.java similarity index 100% rename from combatant-client-26.2/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevGlSyncTracker.java rename to mod/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevGlSyncTracker.java diff --git a/combatant-client-26.2/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevJfrProfiler.java b/mod/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevJfrProfiler.java similarity index 100% rename from combatant-client-26.2/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevJfrProfiler.java rename to mod/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevJfrProfiler.java diff --git a/combatant-client-26.2/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevProfilerPhase.java b/mod/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevProfilerPhase.java similarity index 100% rename from combatant-client-26.2/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevProfilerPhase.java rename to mod/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevProfilerPhase.java diff --git a/combatant-client-26.2/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevRenderCostProfiler.java b/mod/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevRenderCostProfiler.java similarity index 100% rename from combatant-client-26.2/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevRenderCostProfiler.java rename to mod/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevRenderCostProfiler.java diff --git a/combatant-client-26.2/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevRenderFrameProfiler.java b/mod/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevRenderFrameProfiler.java similarity index 100% rename from combatant-client-26.2/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevRenderFrameProfiler.java rename to mod/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevRenderFrameProfiler.java diff --git a/combatant-client-26.2/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevRenderProfiler2D.java b/mod/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevRenderProfiler2D.java similarity index 100% rename from combatant-client-26.2/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevRenderProfiler2D.java rename to mod/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevRenderProfiler2D.java diff --git a/combatant-client-26.2/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevRenderProfiler3D.java b/mod/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevRenderProfiler3D.java similarity index 100% rename from combatant-client-26.2/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevRenderProfiler3D.java rename to mod/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevRenderProfiler3D.java diff --git a/combatant-client-26.2/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevSamplingProfiler.java b/mod/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevSamplingProfiler.java similarity index 100% rename from combatant-client-26.2/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevSamplingProfiler.java rename to mod/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevSamplingProfiler.java diff --git a/combatant-client-26.2/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevTracyGpuProfiler.java b/mod/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevTracyGpuProfiler.java similarity index 100% rename from combatant-client-26.2/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevTracyGpuProfiler.java rename to mod/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevTracyGpuProfiler.java diff --git a/combatant-client-26.2/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevTracyProfiler.java b/mod/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevTracyProfiler.java similarity index 100% rename from combatant-client-26.2/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevTracyProfiler.java rename to mod/src/dev/java/dev/loki/lovisual/render/engine/profiler/DevTracyProfiler.java diff --git a/combatant-client-26.2/src/dev/resources/lovisual-dev.mixins.json b/mod/src/dev/resources/lovisual-dev.mixins.json similarity index 100% rename from combatant-client-26.2/src/dev/resources/lovisual-dev.mixins.json rename to mod/src/dev/resources/lovisual-dev.mixins.json diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/Lifecycle.java b/mod/src/main/java/dev/loki/lovisual/Lifecycle.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/Lifecycle.java rename to mod/src/main/java/dev/loki/lovisual/Lifecycle.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/LoVisual.java b/mod/src/main/java/dev/loki/lovisual/LoVisual.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/LoVisual.java rename to mod/src/main/java/dev/loki/lovisual/LoVisual.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/addon/context/LoVisualAddonContextImpl.java b/mod/src/main/java/dev/loki/lovisual/addon/context/LoVisualAddonContextImpl.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/addon/context/LoVisualAddonContextImpl.java rename to mod/src/main/java/dev/loki/lovisual/addon/context/LoVisualAddonContextImpl.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/addon/context/LoVisualAddonRuntimeContextImpl.java b/mod/src/main/java/dev/loki/lovisual/addon/context/LoVisualAddonRuntimeContextImpl.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/addon/context/LoVisualAddonRuntimeContextImpl.java rename to mod/src/main/java/dev/loki/lovisual/addon/context/LoVisualAddonRuntimeContextImpl.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/addon/context/LoVisualModuleLoadContextImpl.java b/mod/src/main/java/dev/loki/lovisual/addon/context/LoVisualModuleLoadContextImpl.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/addon/context/LoVisualModuleLoadContextImpl.java rename to mod/src/main/java/dev/loki/lovisual/addon/context/LoVisualModuleLoadContextImpl.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/addon/context/ModuleExtensionContextImpl.java b/mod/src/main/java/dev/loki/lovisual/addon/context/ModuleExtensionContextImpl.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/addon/context/ModuleExtensionContextImpl.java rename to mod/src/main/java/dev/loki/lovisual/addon/context/ModuleExtensionContextImpl.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/addon/core/AddonDescriptor.java b/mod/src/main/java/dev/loki/lovisual/addon/core/AddonDescriptor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/addon/core/AddonDescriptor.java rename to mod/src/main/java/dev/loki/lovisual/addon/core/AddonDescriptor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/addon/core/AddonIssue.java b/mod/src/main/java/dev/loki/lovisual/addon/core/AddonIssue.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/addon/core/AddonIssue.java rename to mod/src/main/java/dev/loki/lovisual/addon/core/AddonIssue.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/addon/core/AddonManager.java b/mod/src/main/java/dev/loki/lovisual/addon/core/AddonManager.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/addon/core/AddonManager.java rename to mod/src/main/java/dev/loki/lovisual/addon/core/AddonManager.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/addon/core/AddonRegistration.java b/mod/src/main/java/dev/loki/lovisual/addon/core/AddonRegistration.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/addon/core/AddonRegistration.java rename to mod/src/main/java/dev/loki/lovisual/addon/core/AddonRegistration.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/addon/manager/ClickGuiSectionManager.java b/mod/src/main/java/dev/loki/lovisual/addon/manager/ClickGuiSectionManager.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/addon/manager/ClickGuiSectionManager.java rename to mod/src/main/java/dev/loki/lovisual/addon/manager/ClickGuiSectionManager.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/addon/manager/ModuleExtensionManager.java b/mod/src/main/java/dev/loki/lovisual/addon/manager/ModuleExtensionManager.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/addon/manager/ModuleExtensionManager.java rename to mod/src/main/java/dev/loki/lovisual/addon/manager/ModuleExtensionManager.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/addon/pipeline/AddonRenderPipelineManager.java b/mod/src/main/java/dev/loki/lovisual/addon/pipeline/AddonRenderPipelineManager.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/addon/pipeline/AddonRenderPipelineManager.java rename to mod/src/main/java/dev/loki/lovisual/addon/pipeline/AddonRenderPipelineManager.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/addon/pipeline/AddonSnapshot.java b/mod/src/main/java/dev/loki/lovisual/addon/pipeline/AddonSnapshot.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/addon/pipeline/AddonSnapshot.java rename to mod/src/main/java/dev/loki/lovisual/addon/pipeline/AddonSnapshot.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/addon/pipeline/AddonStateStore.java b/mod/src/main/java/dev/loki/lovisual/addon/pipeline/AddonStateStore.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/addon/pipeline/AddonStateStore.java rename to mod/src/main/java/dev/loki/lovisual/addon/pipeline/AddonStateStore.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/addon/pipeline/AddonStatus.java b/mod/src/main/java/dev/loki/lovisual/addon/pipeline/AddonStatus.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/addon/pipeline/AddonStatus.java rename to mod/src/main/java/dev/loki/lovisual/addon/pipeline/AddonStatus.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/api/v0/addon/LoVisualAddon.java b/mod/src/main/java/dev/loki/lovisual/api/v0/addon/LoVisualAddon.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/api/v0/addon/LoVisualAddon.java rename to mod/src/main/java/dev/loki/lovisual/api/v0/addon/LoVisualAddon.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/api/v0/addon/LoVisualAddonContext.java b/mod/src/main/java/dev/loki/lovisual/api/v0/addon/LoVisualAddonContext.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/api/v0/addon/LoVisualAddonContext.java rename to mod/src/main/java/dev/loki/lovisual/api/v0/addon/LoVisualAddonContext.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/api/v0/addon/LoVisualAddonRuntimeContext.java b/mod/src/main/java/dev/loki/lovisual/api/v0/addon/LoVisualAddonRuntimeContext.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/api/v0/addon/LoVisualAddonRuntimeContext.java rename to mod/src/main/java/dev/loki/lovisual/api/v0/addon/LoVisualAddonRuntimeContext.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/api/v0/addon/LoVisualModuleLoadContext.java b/mod/src/main/java/dev/loki/lovisual/api/v0/addon/LoVisualModuleLoadContext.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/api/v0/addon/LoVisualModuleLoadContext.java rename to mod/src/main/java/dev/loki/lovisual/api/v0/addon/LoVisualModuleLoadContext.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/api/v0/clickgui/LoVisualClickGuiRenderContext.java b/mod/src/main/java/dev/loki/lovisual/api/v0/clickgui/LoVisualClickGuiRenderContext.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/api/v0/clickgui/LoVisualClickGuiRenderContext.java rename to mod/src/main/java/dev/loki/lovisual/api/v0/clickgui/LoVisualClickGuiRenderContext.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/api/v0/clickgui/LoVisualClickGuiSection.java b/mod/src/main/java/dev/loki/lovisual/api/v0/clickgui/LoVisualClickGuiSection.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/api/v0/clickgui/LoVisualClickGuiSection.java rename to mod/src/main/java/dev/loki/lovisual/api/v0/clickgui/LoVisualClickGuiSection.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/api/v0/client/LoVisualClientApi.java b/mod/src/main/java/dev/loki/lovisual/api/v0/client/LoVisualClientApi.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/api/v0/client/LoVisualClientApi.java rename to mod/src/main/java/dev/loki/lovisual/api/v0/client/LoVisualClientApi.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/api/v0/module/LoVisualModuleExtension.java b/mod/src/main/java/dev/loki/lovisual/api/v0/module/LoVisualModuleExtension.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/api/v0/module/LoVisualModuleExtension.java rename to mod/src/main/java/dev/loki/lovisual/api/v0/module/LoVisualModuleExtension.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/api/v0/module/ModuleExtensionContext.java b/mod/src/main/java/dev/loki/lovisual/api/v0/module/ModuleExtensionContext.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/api/v0/module/ModuleExtensionContext.java rename to mod/src/main/java/dev/loki/lovisual/api/v0/module/ModuleExtensionContext.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/api/v0/render/LoVisualRenderPipelineBuilder.java b/mod/src/main/java/dev/loki/lovisual/api/v0/render/LoVisualRenderPipelineBuilder.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/api/v0/render/LoVisualRenderPipelineBuilder.java rename to mod/src/main/java/dev/loki/lovisual/api/v0/render/LoVisualRenderPipelineBuilder.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/api/v0/render/LoVisualRenderStage.java b/mod/src/main/java/dev/loki/lovisual/api/v0/render/LoVisualRenderStage.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/api/v0/render/LoVisualRenderStage.java rename to mod/src/main/java/dev/loki/lovisual/api/v0/render/LoVisualRenderStage.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/api/v0/render/LoVisualUniforms.java b/mod/src/main/java/dev/loki/lovisual/api/v0/render/LoVisualUniforms.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/api/v0/render/LoVisualUniforms.java rename to mod/src/main/java/dev/loki/lovisual/api/v0/render/LoVisualUniforms.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/api/v0/render/callback/LoVisualPostProcessCallback.java b/mod/src/main/java/dev/loki/lovisual/api/v0/render/callback/LoVisualPostProcessCallback.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/api/v0/render/callback/LoVisualPostProcessCallback.java rename to mod/src/main/java/dev/loki/lovisual/api/v0/render/callback/LoVisualPostProcessCallback.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/api/v0/render/callback/LoVisualPostProcessContext.java b/mod/src/main/java/dev/loki/lovisual/api/v0/render/callback/LoVisualPostProcessContext.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/api/v0/render/callback/LoVisualPostProcessContext.java rename to mod/src/main/java/dev/loki/lovisual/api/v0/render/callback/LoVisualPostProcessContext.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/api/v0/render/callback/LoVisualRenderCallback.java b/mod/src/main/java/dev/loki/lovisual/api/v0/render/callback/LoVisualRenderCallback.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/api/v0/render/callback/LoVisualRenderCallback.java rename to mod/src/main/java/dev/loki/lovisual/api/v0/render/callback/LoVisualRenderCallback.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/api/v0/render/callback/LoVisualRenderContext.java b/mod/src/main/java/dev/loki/lovisual/api/v0/render/callback/LoVisualRenderContext.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/api/v0/render/callback/LoVisualRenderContext.java rename to mod/src/main/java/dev/loki/lovisual/api/v0/render/callback/LoVisualRenderContext.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/compat/xaero/XaeroCommandWaypointCompat.java b/mod/src/main/java/dev/loki/lovisual/compat/xaero/XaeroCommandWaypointCompat.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/compat/xaero/XaeroCommandWaypointCompat.java rename to mod/src/main/java/dev/loki/lovisual/compat/xaero/XaeroCommandWaypointCompat.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/compat/xaero/XaeroWaypointRuntime.java b/mod/src/main/java/dev/loki/lovisual/compat/xaero/XaeroWaypointRuntime.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/compat/xaero/XaeroWaypointRuntime.java rename to mod/src/main/java/dev/loki/lovisual/compat/xaero/XaeroWaypointRuntime.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/compat/xaero/XaeroWaypointStore.java b/mod/src/main/java/dev/loki/lovisual/compat/xaero/XaeroWaypointStore.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/compat/xaero/XaeroWaypointStore.java rename to mod/src/main/java/dev/loki/lovisual/compat/xaero/XaeroWaypointStore.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/compat/xaero/triangulator/TriangulatorXaeroMinimapCompat.java b/mod/src/main/java/dev/loki/lovisual/compat/xaero/triangulator/TriangulatorXaeroMinimapCompat.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/compat/xaero/triangulator/TriangulatorXaeroMinimapCompat.java rename to mod/src/main/java/dev/loki/lovisual/compat/xaero/triangulator/TriangulatorXaeroMinimapCompat.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/compat/xaero/triangulator/TriangulatorXaeroState.java b/mod/src/main/java/dev/loki/lovisual/compat/xaero/triangulator/TriangulatorXaeroState.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/compat/xaero/triangulator/TriangulatorXaeroState.java rename to mod/src/main/java/dev/loki/lovisual/compat/xaero/triangulator/TriangulatorXaeroState.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/compat/xaero/triangulator/TriangulatorXaeroWorldMapCompat.java b/mod/src/main/java/dev/loki/lovisual/compat/xaero/triangulator/TriangulatorXaeroWorldMapCompat.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/compat/xaero/triangulator/TriangulatorXaeroWorldMapCompat.java rename to mod/src/main/java/dev/loki/lovisual/compat/xaero/triangulator/TriangulatorXaeroWorldMapCompat.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/common/CommonBooleanGroupSchema.java b/mod/src/main/java/dev/loki/lovisual/config/common/CommonBooleanGroupSchema.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/common/CommonBooleanGroupSchema.java rename to mod/src/main/java/dev/loki/lovisual/config/common/CommonBooleanGroupSchema.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/common/CommonSettingSchema.java b/mod/src/main/java/dev/loki/lovisual/config/common/CommonSettingSchema.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/common/CommonSettingSchema.java rename to mod/src/main/java/dev/loki/lovisual/config/common/CommonSettingSchema.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/common/CommonSettingSchemas.java b/mod/src/main/java/dev/loki/lovisual/config/common/CommonSettingSchemas.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/common/CommonSettingSchemas.java rename to mod/src/main/java/dev/loki/lovisual/config/common/CommonSettingSchemas.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/common/impl/common/CombatPriority.java b/mod/src/main/java/dev/loki/lovisual/config/common/impl/common/CombatPriority.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/common/impl/common/CombatPriority.java rename to mod/src/main/java/dev/loki/lovisual/config/common/impl/common/CombatPriority.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/common/impl/common/CombatProtocolHeuristicSources.java b/mod/src/main/java/dev/loki/lovisual/config/common/impl/common/CombatProtocolHeuristicSources.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/common/impl/common/CombatProtocolHeuristicSources.java rename to mod/src/main/java/dev/loki/lovisual/config/common/impl/common/CombatProtocolHeuristicSources.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/common/impl/common/CommonKey.java b/mod/src/main/java/dev/loki/lovisual/config/common/impl/common/CommonKey.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/common/impl/common/CommonKey.java rename to mod/src/main/java/dev/loki/lovisual/config/common/impl/common/CommonKey.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/common/impl/common/FakeLagFlushOn.java b/mod/src/main/java/dev/loki/lovisual/config/common/impl/common/FakeLagFlushOn.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/common/impl/common/FakeLagFlushOn.java rename to mod/src/main/java/dev/loki/lovisual/config/common/impl/common/FakeLagFlushOn.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/common/impl/rotation/RotationMode.java b/mod/src/main/java/dev/loki/lovisual/config/common/impl/rotation/RotationMode.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/common/impl/rotation/RotationMode.java rename to mod/src/main/java/dev/loki/lovisual/config/common/impl/rotation/RotationMode.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/common/impl/rotation/RotationMovementCorrection.java b/mod/src/main/java/dev/loki/lovisual/config/common/impl/rotation/RotationMovementCorrection.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/common/impl/rotation/RotationMovementCorrection.java rename to mod/src/main/java/dev/loki/lovisual/config/common/impl/rotation/RotationMovementCorrection.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/common/impl/rotation/RotationResetThreshold.java b/mod/src/main/java/dev/loki/lovisual/config/common/impl/rotation/RotationResetThreshold.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/common/impl/rotation/RotationResetThreshold.java rename to mod/src/main/java/dev/loki/lovisual/config/common/impl/rotation/RotationResetThreshold.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/common/impl/rotation/RotationResetTicks.java b/mod/src/main/java/dev/loki/lovisual/config/common/impl/rotation/RotationResetTicks.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/common/impl/rotation/RotationResetTicks.java rename to mod/src/main/java/dev/loki/lovisual/config/common/impl/rotation/RotationResetTicks.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/common/impl/wall/RotationThroughWalls.java b/mod/src/main/java/dev/loki/lovisual/config/common/impl/wall/RotationThroughWalls.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/common/impl/wall/RotationThroughWalls.java rename to mod/src/main/java/dev/loki/lovisual/config/common/impl/wall/RotationThroughWalls.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/common/impl/wall/RotationTiming.java b/mod/src/main/java/dev/loki/lovisual/config/common/impl/wall/RotationTiming.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/common/impl/wall/RotationTiming.java rename to mod/src/main/java/dev/loki/lovisual/config/common/impl/wall/RotationTiming.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/common/impl/wall/TargetFilters.java b/mod/src/main/java/dev/loki/lovisual/config/common/impl/wall/TargetFilters.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/common/impl/wall/TargetFilters.java rename to mod/src/main/java/dev/loki/lovisual/config/common/impl/wall/TargetFilters.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/definition/ConfigNameProvider.java b/mod/src/main/java/dev/loki/lovisual/config/definition/ConfigNameProvider.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/definition/ConfigNameProvider.java rename to mod/src/main/java/dev/loki/lovisual/config/definition/ConfigNameProvider.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/definition/ConfigObject.java b/mod/src/main/java/dev/loki/lovisual/config/definition/ConfigObject.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/definition/ConfigObject.java rename to mod/src/main/java/dev/loki/lovisual/config/definition/ConfigObject.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/definition/SettingDef.java b/mod/src/main/java/dev/loki/lovisual/config/definition/SettingDef.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/definition/SettingDef.java rename to mod/src/main/java/dev/loki/lovisual/config/definition/SettingDef.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/definition/SettingOwner.java b/mod/src/main/java/dev/loki/lovisual/config/definition/SettingOwner.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/definition/SettingOwner.java rename to mod/src/main/java/dev/loki/lovisual/config/definition/SettingOwner.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/i18n/ConfigLang.java b/mod/src/main/java/dev/loki/lovisual/config/i18n/ConfigLang.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/i18n/ConfigLang.java rename to mod/src/main/java/dev/loki/lovisual/config/i18n/ConfigLang.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/i18n/ConfigValueAliasProvider.java b/mod/src/main/java/dev/loki/lovisual/config/i18n/ConfigValueAliasProvider.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/i18n/ConfigValueAliasProvider.java rename to mod/src/main/java/dev/loki/lovisual/config/i18n/ConfigValueAliasProvider.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/i18n/DisableSettingI18n.java b/mod/src/main/java/dev/loki/lovisual/config/i18n/DisableSettingI18n.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/i18n/DisableSettingI18n.java rename to mod/src/main/java/dev/loki/lovisual/config/i18n/DisableSettingI18n.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/i18n/MainConfig.java b/mod/src/main/java/dev/loki/lovisual/config/i18n/MainConfig.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/i18n/MainConfig.java rename to mod/src/main/java/dev/loki/lovisual/config/i18n/MainConfig.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/io/ConfigPaths.java b/mod/src/main/java/dev/loki/lovisual/config/io/ConfigPaths.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/io/ConfigPaths.java rename to mod/src/main/java/dev/loki/lovisual/config/io/ConfigPaths.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/io/ConfigSerializer.java b/mod/src/main/java/dev/loki/lovisual/config/io/ConfigSerializer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/io/ConfigSerializer.java rename to mod/src/main/java/dev/loki/lovisual/config/io/ConfigSerializer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/io/JsonConfigObject.java b/mod/src/main/java/dev/loki/lovisual/config/io/JsonConfigObject.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/io/JsonConfigObject.java rename to mod/src/main/java/dev/loki/lovisual/config/io/JsonConfigObject.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/io/SettingDefProvider.java b/mod/src/main/java/dev/loki/lovisual/config/io/SettingDefProvider.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/io/SettingDefProvider.java rename to mod/src/main/java/dev/loki/lovisual/config/io/SettingDefProvider.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/profile/codec/ConfigProfileBinaryCodec.java b/mod/src/main/java/dev/loki/lovisual/config/profile/codec/ConfigProfileBinaryCodec.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/profile/codec/ConfigProfileBinaryCodec.java rename to mod/src/main/java/dev/loki/lovisual/config/profile/codec/ConfigProfileBinaryCodec.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/profile/codec/ConfigProfileEntry.java b/mod/src/main/java/dev/loki/lovisual/config/profile/codec/ConfigProfileEntry.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/profile/codec/ConfigProfileEntry.java rename to mod/src/main/java/dev/loki/lovisual/config/profile/codec/ConfigProfileEntry.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/profile/codec/ConfigProfileType.java b/mod/src/main/java/dev/loki/lovisual/config/profile/codec/ConfigProfileType.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/profile/codec/ConfigProfileType.java rename to mod/src/main/java/dev/loki/lovisual/config/profile/codec/ConfigProfileType.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/profile/codec/ConfigProfileValueCodec.java b/mod/src/main/java/dev/loki/lovisual/config/profile/codec/ConfigProfileValueCodec.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/profile/codec/ConfigProfileValueCodec.java rename to mod/src/main/java/dev/loki/lovisual/config/profile/codec/ConfigProfileValueCodec.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/profile/diff/ConfigProfileDateFormatter.java b/mod/src/main/java/dev/loki/lovisual/config/profile/diff/ConfigProfileDateFormatter.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/profile/diff/ConfigProfileDateFormatter.java rename to mod/src/main/java/dev/loki/lovisual/config/profile/diff/ConfigProfileDateFormatter.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/profile/diff/ConfigProfileDiff.java b/mod/src/main/java/dev/loki/lovisual/config/profile/diff/ConfigProfileDiff.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/profile/diff/ConfigProfileDiff.java rename to mod/src/main/java/dev/loki/lovisual/config/profile/diff/ConfigProfileDiff.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/profile/diff/ConfigProfileDiffEngine.java b/mod/src/main/java/dev/loki/lovisual/config/profile/diff/ConfigProfileDiffEngine.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/profile/diff/ConfigProfileDiffEngine.java rename to mod/src/main/java/dev/loki/lovisual/config/profile/diff/ConfigProfileDiffEngine.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/profile/service/ConfigProfileApplier.java b/mod/src/main/java/dev/loki/lovisual/config/profile/service/ConfigProfileApplier.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/profile/service/ConfigProfileApplier.java rename to mod/src/main/java/dev/loki/lovisual/config/profile/service/ConfigProfileApplier.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/profile/service/ConfigProfileCollector.java b/mod/src/main/java/dev/loki/lovisual/config/profile/service/ConfigProfileCollector.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/profile/service/ConfigProfileCollector.java rename to mod/src/main/java/dev/loki/lovisual/config/profile/service/ConfigProfileCollector.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/profile/service/ConfigProfileService.java b/mod/src/main/java/dev/loki/lovisual/config/profile/service/ConfigProfileService.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/profile/service/ConfigProfileService.java rename to mod/src/main/java/dev/loki/lovisual/config/profile/service/ConfigProfileService.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/profile/service/ConfigProfileStorage.java b/mod/src/main/java/dev/loki/lovisual/config/profile/service/ConfigProfileStorage.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/profile/service/ConfigProfileStorage.java rename to mod/src/main/java/dev/loki/lovisual/config/profile/service/ConfigProfileStorage.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/profile/snapshot/ConfigProfileMeta.java b/mod/src/main/java/dev/loki/lovisual/config/profile/snapshot/ConfigProfileMeta.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/profile/snapshot/ConfigProfileMeta.java rename to mod/src/main/java/dev/loki/lovisual/config/profile/snapshot/ConfigProfileMeta.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/profile/snapshot/ConfigProfileSnapshot.java b/mod/src/main/java/dev/loki/lovisual/config/profile/snapshot/ConfigProfileSnapshot.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/profile/snapshot/ConfigProfileSnapshot.java rename to mod/src/main/java/dev/loki/lovisual/config/profile/snapshot/ConfigProfileSnapshot.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/values/collection/ConfigValue.java b/mod/src/main/java/dev/loki/lovisual/config/values/collection/ConfigValue.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/values/collection/ConfigValue.java rename to mod/src/main/java/dev/loki/lovisual/config/values/collection/ConfigValue.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/values/collection/ItemCooldownRulesValue.java b/mod/src/main/java/dev/loki/lovisual/config/values/collection/ItemCooldownRulesValue.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/values/collection/ItemCooldownRulesValue.java rename to mod/src/main/java/dev/loki/lovisual/config/values/collection/ItemCooldownRulesValue.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/values/collection/ItemIdSetValue.java b/mod/src/main/java/dev/loki/lovisual/config/values/collection/ItemIdSetValue.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/values/collection/ItemIdSetValue.java rename to mod/src/main/java/dev/loki/lovisual/config/values/collection/ItemIdSetValue.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/values/collection/SetValue.java b/mod/src/main/java/dev/loki/lovisual/config/values/collection/SetValue.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/values/collection/SetValue.java rename to mod/src/main/java/dev/loki/lovisual/config/values/collection/SetValue.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/values/color/ColorValue.java b/mod/src/main/java/dev/loki/lovisual/config/values/color/ColorValue.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/values/color/ColorValue.java rename to mod/src/main/java/dev/loki/lovisual/config/values/color/ColorValue.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/values/color/KeyBindValue.java b/mod/src/main/java/dev/loki/lovisual/config/values/color/KeyBindValue.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/values/color/KeyBindValue.java rename to mod/src/main/java/dev/loki/lovisual/config/values/color/KeyBindValue.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/values/color/RGBAColorValue.java b/mod/src/main/java/dev/loki/lovisual/config/values/color/RGBAColorValue.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/values/color/RGBAColorValue.java rename to mod/src/main/java/dev/loki/lovisual/config/values/color/RGBAColorValue.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/values/color/RGBColorValue.java b/mod/src/main/java/dev/loki/lovisual/config/values/color/RGBColorValue.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/values/color/RGBColorValue.java rename to mod/src/main/java/dev/loki/lovisual/config/values/color/RGBColorValue.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/values/mode/BindMode.java b/mod/src/main/java/dev/loki/lovisual/config/values/mode/BindMode.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/values/mode/BindMode.java rename to mod/src/main/java/dev/loki/lovisual/config/values/mode/BindMode.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/values/mode/EnumIds.java b/mod/src/main/java/dev/loki/lovisual/config/values/mode/EnumIds.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/values/mode/EnumIds.java rename to mod/src/main/java/dev/loki/lovisual/config/values/mode/EnumIds.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/values/mode/EnumValue.java b/mod/src/main/java/dev/loki/lovisual/config/values/mode/EnumValue.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/values/mode/EnumValue.java rename to mod/src/main/java/dev/loki/lovisual/config/values/mode/EnumValue.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/values/mode/ModeValue.java b/mod/src/main/java/dev/loki/lovisual/config/values/mode/ModeValue.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/values/mode/ModeValue.java rename to mod/src/main/java/dev/loki/lovisual/config/values/mode/ModeValue.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/values/primitive/BooleanMapValue.java b/mod/src/main/java/dev/loki/lovisual/config/values/primitive/BooleanMapValue.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/values/primitive/BooleanMapValue.java rename to mod/src/main/java/dev/loki/lovisual/config/values/primitive/BooleanMapValue.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/values/primitive/BooleanValue.java b/mod/src/main/java/dev/loki/lovisual/config/values/primitive/BooleanValue.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/values/primitive/BooleanValue.java rename to mod/src/main/java/dev/loki/lovisual/config/values/primitive/BooleanValue.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/values/primitive/NumberValue.java b/mod/src/main/java/dev/loki/lovisual/config/values/primitive/NumberValue.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/values/primitive/NumberValue.java rename to mod/src/main/java/dev/loki/lovisual/config/values/primitive/NumberValue.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/values/primitive/StringValue.java b/mod/src/main/java/dev/loki/lovisual/config/values/primitive/StringValue.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/config/values/primitive/StringValue.java rename to mod/src/main/java/dev/loki/lovisual/config/values/primitive/StringValue.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/Event.java b/mod/src/main/java/dev/loki/lovisual/events/Event.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/events/Event.java rename to mod/src/main/java/dev/loki/lovisual/events/Event.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/EventBus.java b/mod/src/main/java/dev/loki/lovisual/events/EventBus.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/events/EventBus.java rename to mod/src/main/java/dev/loki/lovisual/events/EventBus.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/Events.java b/mod/src/main/java/dev/loki/lovisual/events/Events.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/events/Events.java rename to mod/src/main/java/dev/loki/lovisual/events/Events.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/entity/AttackEntityEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/entity/AttackEntityEvent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/entity/AttackEntityEvent.java rename to mod/src/main/java/dev/loki/lovisual/events/impl/entity/AttackEntityEvent.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/entity/CrosshairTargetUpdateEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/entity/CrosshairTargetUpdateEvent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/entity/CrosshairTargetUpdateEvent.java rename to mod/src/main/java/dev/loki/lovisual/events/impl/entity/CrosshairTargetUpdateEvent.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/entity/EventTargetChanged.java b/mod/src/main/java/dev/loki/lovisual/events/impl/entity/EventTargetChanged.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/entity/EventTargetChanged.java rename to mod/src/main/java/dev/loki/lovisual/events/impl/entity/EventTargetChanged.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/entity/FireworkEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/entity/FireworkEvent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/entity/FireworkEvent.java rename to mod/src/main/java/dev/loki/lovisual/events/impl/entity/FireworkEvent.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/input/KeyInputEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/input/KeyInputEvent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/input/KeyInputEvent.java rename to mod/src/main/java/dev/loki/lovisual/events/impl/input/KeyInputEvent.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/input/KeybindIsPressedEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/input/KeybindIsPressedEvent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/input/KeybindIsPressedEvent.java rename to mod/src/main/java/dev/loki/lovisual/events/impl/input/KeybindIsPressedEvent.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/input/RotationUpdateEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/input/RotationUpdateEvent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/input/RotationUpdateEvent.java rename to mod/src/main/java/dev/loki/lovisual/events/impl/input/RotationUpdateEvent.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/input/SprintControlEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/input/SprintControlEvent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/input/SprintControlEvent.java rename to mod/src/main/java/dev/loki/lovisual/events/impl/input/SprintControlEvent.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/movement/MovementInputEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/movement/MovementInputEvent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/movement/MovementInputEvent.java rename to mod/src/main/java/dev/loki/lovisual/events/impl/movement/MovementInputEvent.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/movement/PlayerSafeWalkEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/movement/PlayerSafeWalkEvent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/movement/PlayerSafeWalkEvent.java rename to mod/src/main/java/dev/loki/lovisual/events/impl/movement/PlayerSafeWalkEvent.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/movement/PlayerStepEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/movement/PlayerStepEvent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/movement/PlayerStepEvent.java rename to mod/src/main/java/dev/loki/lovisual/events/impl/movement/PlayerStepEvent.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/movement/PlayerStepSuccessEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/movement/PlayerStepSuccessEvent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/movement/PlayerStepSuccessEvent.java rename to mod/src/main/java/dev/loki/lovisual/events/impl/movement/PlayerStepSuccessEvent.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/network/BlinkPacketEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/network/BlinkPacketEvent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/network/BlinkPacketEvent.java rename to mod/src/main/java/dev/loki/lovisual/events/impl/network/BlinkPacketEvent.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/network/EventPostSync.java b/mod/src/main/java/dev/loki/lovisual/events/impl/network/EventPostSync.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/network/EventPostSync.java rename to mod/src/main/java/dev/loki/lovisual/events/impl/network/EventPostSync.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/network/EventSync.java b/mod/src/main/java/dev/loki/lovisual/events/impl/network/EventSync.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/network/EventSync.java rename to mod/src/main/java/dev/loki/lovisual/events/impl/network/EventSync.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/network/PacketEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/network/PacketEvent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/network/PacketEvent.java rename to mod/src/main/java/dev/loki/lovisual/events/impl/network/PacketEvent.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/player/I18nPreflightCollectEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/player/I18nPreflightCollectEvent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/player/I18nPreflightCollectEvent.java rename to mod/src/main/java/dev/loki/lovisual/events/impl/player/I18nPreflightCollectEvent.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/player/PlayerJumpEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/player/PlayerJumpEvent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/player/PlayerJumpEvent.java rename to mod/src/main/java/dev/loki/lovisual/events/impl/player/PlayerJumpEvent.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/player/PlayerMoveEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/player/PlayerMoveEvent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/player/PlayerMoveEvent.java rename to mod/src/main/java/dev/loki/lovisual/events/impl/player/PlayerMoveEvent.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/player/PostPlayerUpdateEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/player/PostPlayerUpdateEvent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/player/PostPlayerUpdateEvent.java rename to mod/src/main/java/dev/loki/lovisual/events/impl/player/PostPlayerUpdateEvent.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/pvp/CombatProtocolBossbarEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/pvp/CombatProtocolBossbarEvent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/pvp/CombatProtocolBossbarEvent.java rename to mod/src/main/java/dev/loki/lovisual/events/impl/pvp/CombatProtocolBossbarEvent.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/pvp/PvpChatEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/pvp/PvpChatEvent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/pvp/PvpChatEvent.java rename to mod/src/main/java/dev/loki/lovisual/events/impl/pvp/PvpChatEvent.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/pvp/PvpOverlayEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/pvp/PvpOverlayEvent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/pvp/PvpOverlayEvent.java rename to mod/src/main/java/dev/loki/lovisual/events/impl/pvp/PvpOverlayEvent.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/pvp/PvpTabEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/pvp/PvpTabEvent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/pvp/PvpTabEvent.java rename to mod/src/main/java/dev/loki/lovisual/events/impl/pvp/PvpTabEvent.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/render/GameTickEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/render/GameTickEvent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/render/GameTickEvent.java rename to mod/src/main/java/dev/loki/lovisual/events/impl/render/GameTickEvent.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/render/LightmapEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/render/LightmapEvent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/render/LightmapEvent.java rename to mod/src/main/java/dev/loki/lovisual/events/impl/render/LightmapEvent.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/render/LightmapModifyEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/render/LightmapModifyEvent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/render/LightmapModifyEvent.java rename to mod/src/main/java/dev/loki/lovisual/events/impl/render/LightmapModifyEvent.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/render/RenderPrewarmCollectEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/render/RenderPrewarmCollectEvent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/render/RenderPrewarmCollectEvent.java rename to mod/src/main/java/dev/loki/lovisual/events/impl/render/RenderPrewarmCollectEvent.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/render/particles/ParticleSpawnEvent.java b/mod/src/main/java/dev/loki/lovisual/events/impl/render/particles/ParticleSpawnEvent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/render/particles/ParticleSpawnEvent.java rename to mod/src/main/java/dev/loki/lovisual/events/impl/render/particles/ParticleSpawnEvent.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/world/EventBreakBlock.java b/mod/src/main/java/dev/loki/lovisual/events/impl/world/EventBreakBlock.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/world/EventBreakBlock.java rename to mod/src/main/java/dev/loki/lovisual/events/impl/world/EventBreakBlock.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/world/EventCollision.java b/mod/src/main/java/dev/loki/lovisual/events/impl/world/EventCollision.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/world/EventCollision.java rename to mod/src/main/java/dev/loki/lovisual/events/impl/world/EventCollision.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/world/EventPushOutOfBlocks.java b/mod/src/main/java/dev/loki/lovisual/events/impl/world/EventPushOutOfBlocks.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/world/EventPushOutOfBlocks.java rename to mod/src/main/java/dev/loki/lovisual/events/impl/world/EventPushOutOfBlocks.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/world/PlayerVelocityStrafe.java b/mod/src/main/java/dev/loki/lovisual/events/impl/world/PlayerVelocityStrafe.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/world/PlayerVelocityStrafe.java rename to mod/src/main/java/dev/loki/lovisual/events/impl/world/PlayerVelocityStrafe.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/meta/EventHandler.java b/mod/src/main/java/dev/loki/lovisual/events/meta/EventHandler.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/events/meta/EventHandler.java rename to mod/src/main/java/dev/loki/lovisual/events/meta/EventHandler.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/meta/UsedImplicitly.java b/mod/src/main/java/dev/loki/lovisual/events/meta/UsedImplicitly.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/events/meta/UsedImplicitly.java rename to mod/src/main/java/dev/loki/lovisual/events/meta/UsedImplicitly.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/account/AccountAuthType.java b/mod/src/main/java/dev/loki/lovisual/features/account/AccountAuthType.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/account/AccountAuthType.java rename to mod/src/main/java/dev/loki/lovisual/features/account/AccountAuthType.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/account/AccountConfig.java b/mod/src/main/java/dev/loki/lovisual/features/account/AccountConfig.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/account/AccountConfig.java rename to mod/src/main/java/dev/loki/lovisual/features/account/AccountConfig.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/account/AccountEntry.java b/mod/src/main/java/dev/loki/lovisual/features/account/AccountEntry.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/account/AccountEntry.java rename to mod/src/main/java/dev/loki/lovisual/features/account/AccountEntry.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/account/SkinManager.java b/mod/src/main/java/dev/loki/lovisual/features/account/SkinManager.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/account/SkinManager.java rename to mod/src/main/java/dev/loki/lovisual/features/account/SkinManager.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/core/CommandContext.java b/mod/src/main/java/dev/loki/lovisual/features/command/core/CommandContext.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/core/CommandContext.java rename to mod/src/main/java/dev/loki/lovisual/features/command/core/CommandContext.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/core/CommandInfo.java b/mod/src/main/java/dev/loki/lovisual/features/command/core/CommandInfo.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/core/CommandInfo.java rename to mod/src/main/java/dev/loki/lovisual/features/command/core/CommandInfo.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/core/CommandManager.java b/mod/src/main/java/dev/loki/lovisual/features/command/core/CommandManager.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/core/CommandManager.java rename to mod/src/main/java/dev/loki/lovisual/features/command/core/CommandManager.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/core/CommandMetadata.java b/mod/src/main/java/dev/loki/lovisual/features/command/core/CommandMetadata.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/core/CommandMetadata.java rename to mod/src/main/java/dev/loki/lovisual/features/command/core/CommandMetadata.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/CommandUtils.java b/mod/src/main/java/dev/loki/lovisual/features/command/impl/CommandUtils.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/CommandUtils.java rename to mod/src/main/java/dev/loki/lovisual/features/command/impl/CommandUtils.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/client/AddonsCommand.java b/mod/src/main/java/dev/loki/lovisual/features/command/impl/client/AddonsCommand.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/client/AddonsCommand.java rename to mod/src/main/java/dev/loki/lovisual/features/command/impl/client/AddonsCommand.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/client/ConfigCommand.java b/mod/src/main/java/dev/loki/lovisual/features/command/impl/client/ConfigCommand.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/client/ConfigCommand.java rename to mod/src/main/java/dev/loki/lovisual/features/command/impl/client/ConfigCommand.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/client/RelationCommand.java b/mod/src/main/java/dev/loki/lovisual/features/command/impl/client/RelationCommand.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/client/RelationCommand.java rename to mod/src/main/java/dev/loki/lovisual/features/command/impl/client/RelationCommand.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/client/UsernameCommand.java b/mod/src/main/java/dev/loki/lovisual/features/command/impl/client/UsernameCommand.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/client/UsernameCommand.java rename to mod/src/main/java/dev/loki/lovisual/features/command/impl/client/UsernameCommand.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/info/HelpCommand.java b/mod/src/main/java/dev/loki/lovisual/features/command/impl/info/HelpCommand.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/info/HelpCommand.java rename to mod/src/main/java/dev/loki/lovisual/features/command/impl/info/HelpCommand.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/info/PingCommand.java b/mod/src/main/java/dev/loki/lovisual/features/command/impl/info/PingCommand.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/info/PingCommand.java rename to mod/src/main/java/dev/loki/lovisual/features/command/impl/info/PingCommand.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/info/ServerInfoCommand.java b/mod/src/main/java/dev/loki/lovisual/features/command/impl/info/ServerInfoCommand.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/info/ServerInfoCommand.java rename to mod/src/main/java/dev/loki/lovisual/features/command/impl/info/ServerInfoCommand.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/info/TpsCommand.java b/mod/src/main/java/dev/loki/lovisual/features/command/impl/info/TpsCommand.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/info/TpsCommand.java rename to mod/src/main/java/dev/loki/lovisual/features/command/impl/info/TpsCommand.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/module/BindCommand.java b/mod/src/main/java/dev/loki/lovisual/features/command/impl/module/BindCommand.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/module/BindCommand.java rename to mod/src/main/java/dev/loki/lovisual/features/command/impl/module/BindCommand.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/module/HideCommand.java b/mod/src/main/java/dev/loki/lovisual/features/command/impl/module/HideCommand.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/module/HideCommand.java rename to mod/src/main/java/dev/loki/lovisual/features/command/impl/module/HideCommand.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/module/ModulesCommand.java b/mod/src/main/java/dev/loki/lovisual/features/command/impl/module/ModulesCommand.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/module/ModulesCommand.java rename to mod/src/main/java/dev/loki/lovisual/features/command/impl/module/ModulesCommand.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/module/ToggleCommand.java b/mod/src/main/java/dev/loki/lovisual/features/command/impl/module/ToggleCommand.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/module/ToggleCommand.java rename to mod/src/main/java/dev/loki/lovisual/features/command/impl/module/ToggleCommand.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/world/CoordinatesCommand.java b/mod/src/main/java/dev/loki/lovisual/features/command/impl/world/CoordinatesCommand.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/world/CoordinatesCommand.java rename to mod/src/main/java/dev/loki/lovisual/features/command/impl/world/CoordinatesCommand.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/world/GpsCommand.java b/mod/src/main/java/dev/loki/lovisual/features/command/impl/world/GpsCommand.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/world/GpsCommand.java rename to mod/src/main/java/dev/loki/lovisual/features/command/impl/world/GpsCommand.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/world/TierCommand.java b/mod/src/main/java/dev/loki/lovisual/features/command/impl/world/TierCommand.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/world/TierCommand.java rename to mod/src/main/java/dev/loki/lovisual/features/command/impl/world/TierCommand.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/world/integration/IrisCommand.java b/mod/src/main/java/dev/loki/lovisual/features/command/impl/world/integration/IrisCommand.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/world/integration/IrisCommand.java rename to mod/src/main/java/dev/loki/lovisual/features/command/impl/world/integration/IrisCommand.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/world/integration/XaeroWaypointCommand.java b/mod/src/main/java/dev/loki/lovisual/features/command/impl/world/integration/XaeroWaypointCommand.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/world/integration/XaeroWaypointCommand.java rename to mod/src/main/java/dev/loki/lovisual/features/command/impl/world/integration/XaeroWaypointCommand.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/util/ClientCommand.java b/mod/src/main/java/dev/loki/lovisual/features/command/util/ClientCommand.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/util/ClientCommand.java rename to mod/src/main/java/dev/loki/lovisual/features/command/util/ClientCommand.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/util/CommandOutput.java b/mod/src/main/java/dev/loki/lovisual/features/command/util/CommandOutput.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/util/CommandOutput.java rename to mod/src/main/java/dev/loki/lovisual/features/command/util/CommandOutput.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/util/CommandPrefix.java b/mod/src/main/java/dev/loki/lovisual/features/command/util/CommandPrefix.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/util/CommandPrefix.java rename to mod/src/main/java/dev/loki/lovisual/features/command/util/CommandPrefix.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/util/UsageHints.java b/mod/src/main/java/dev/loki/lovisual/features/command/util/UsageHints.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/util/UsageHints.java rename to mod/src/main/java/dev/loki/lovisual/features/command/util/UsageHints.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/BetterChatViewport.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/BetterChatViewport.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/BetterChatViewport.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/chat/BetterChatViewport.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/input/BetterChatEditBoxUtil.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/input/BetterChatEditBoxUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/input/BetterChatEditBoxUtil.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/chat/input/BetterChatEditBoxUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/input/BetterChatInputHandler.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/input/BetterChatInputHandler.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/input/BetterChatInputHandler.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/chat/input/BetterChatInputHandler.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/input/BetterChatInputRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/input/BetterChatInputRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/input/BetterChatInputRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/chat/input/BetterChatInputRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/input/BetterChatMouseClickHandler.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/input/BetterChatMouseClickHandler.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/input/BetterChatMouseClickHandler.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/chat/input/BetterChatMouseClickHandler.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/menu/BetterChatContextMenuHandler.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/menu/BetterChatContextMenuHandler.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/menu/BetterChatContextMenuHandler.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/chat/menu/BetterChatContextMenuHandler.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/menu/BetterChatContextMenuModel.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/menu/BetterChatContextMenuModel.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/menu/BetterChatContextMenuModel.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/chat/menu/BetterChatContextMenuModel.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/menu/BetterChatTooltipRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/menu/BetterChatTooltipRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/menu/BetterChatTooltipRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/chat/menu/BetterChatTooltipRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/messages/BetterChatMessageGrouping.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/messages/BetterChatMessageGrouping.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/messages/BetterChatMessageGrouping.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/chat/messages/BetterChatMessageGrouping.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/messages/BetterChatMessageRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/messages/BetterChatMessageRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/messages/BetterChatMessageRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/chat/messages/BetterChatMessageRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/messages/BetterChatSearch.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/messages/BetterChatSearch.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/messages/BetterChatSearch.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/chat/messages/BetterChatSearch.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/messages/BetterChatTabHandler.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/messages/BetterChatTabHandler.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/messages/BetterChatTabHandler.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/chat/messages/BetterChatTabHandler.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/model/BetterChatModel.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/model/BetterChatModel.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/model/BetterChatModel.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/chat/model/BetterChatModel.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/model/BetterChatStore.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/model/BetterChatStore.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/model/BetterChatStore.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/chat/model/BetterChatStore.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/model/BetterChatStoreManager.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/model/BetterChatStoreManager.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/model/BetterChatStoreManager.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/chat/model/BetterChatStoreManager.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/model/ChatLine.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/model/ChatLine.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/model/ChatLine.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/chat/model/ChatLine.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/render/BetterChatFrameModel.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/render/BetterChatFrameModel.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/render/BetterChatFrameModel.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/chat/render/BetterChatFrameModel.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/render/BetterChatFrameRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/render/BetterChatFrameRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/render/BetterChatFrameRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/chat/render/BetterChatFrameRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/render/BetterChatRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/render/BetterChatRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/render/BetterChatRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/chat/render/BetterChatRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/suggestions/BetterChatCommandMerge.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/suggestions/BetterChatCommandMerge.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/suggestions/BetterChatCommandMerge.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/chat/suggestions/BetterChatCommandMerge.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/suggestions/BetterChatSuggestionRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/suggestions/BetterChatSuggestionRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/suggestions/BetterChatSuggestionRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/chat/suggestions/BetterChatSuggestionRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/suggestions/BetterChatSuggestionState.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/suggestions/BetterChatSuggestionState.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/suggestions/BetterChatSuggestionState.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/chat/suggestions/BetterChatSuggestionState.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/suggestions/CommandSuggestorBridge.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/suggestions/CommandSuggestorBridge.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/suggestions/CommandSuggestorBridge.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/chat/suggestions/CommandSuggestorBridge.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/text/BetterChatGlyphRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/BetterChatGlyphRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/text/BetterChatGlyphRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/BetterChatGlyphRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/text/BetterChatLayoutEngine.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/BetterChatLayoutEngine.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/text/BetterChatLayoutEngine.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/BetterChatLayoutEngine.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/text/BetterChatLineLayout.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/BetterChatLineLayout.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/text/BetterChatLineLayout.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/BetterChatLineLayout.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/text/BetterChatSegmentUtil.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/BetterChatSegmentUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/text/BetterChatSegmentUtil.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/BetterChatSegmentUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/text/util/BetterChatDrawUtil.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/util/BetterChatDrawUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/text/util/BetterChatDrawUtil.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/util/BetterChatDrawUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/text/util/BetterChatFontUtil.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/util/BetterChatFontUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/text/util/BetterChatFontUtil.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/util/BetterChatFontUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/text/util/BetterChatHoverCache.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/util/BetterChatHoverCache.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/text/util/BetterChatHoverCache.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/util/BetterChatHoverCache.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/text/util/ChatHoverUtil.java b/mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/util/ChatHoverUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/text/util/ChatHoverUtil.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/chat/text/util/ChatHoverUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/ClickGuiModel.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/ClickGuiModel.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/ClickGuiModel.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/ClickGuiModel.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/editor/ClickGuiEditorManager.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/editor/ClickGuiEditorManager.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/editor/ClickGuiEditorManager.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/editor/ClickGuiEditorManager.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/editor/ClickGuiEditorScreen.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/editor/ClickGuiEditorScreen.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/editor/ClickGuiEditorScreen.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/editor/ClickGuiEditorScreen.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/editor/ClickGuiTextEditorState.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/editor/ClickGuiTextEditorState.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/editor/ClickGuiTextEditorState.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/editor/ClickGuiTextEditorState.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/font/ClickGuiFonts.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/font/ClickGuiFonts.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/font/ClickGuiFonts.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/font/ClickGuiFonts.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/input/ClickGuiInputHandler.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/input/ClickGuiInputHandler.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/input/ClickGuiInputHandler.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/input/ClickGuiInputHandler.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/input/ClickGuiKeyBindManager.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/input/ClickGuiKeyBindManager.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/input/ClickGuiKeyBindManager.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/input/ClickGuiKeyBindManager.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/input/ClickGuiKeyboardHandler.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/input/ClickGuiKeyboardHandler.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/input/ClickGuiKeyboardHandler.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/input/ClickGuiKeyboardHandler.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/ModulesMenuPanel.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/ModulesMenuPanel.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/ModulesMenuPanel.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/ModulesMenuPanel.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/ModulesMenuResolver.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/ModulesMenuResolver.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/ModulesMenuResolver.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/ModulesMenuResolver.java diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/ModulesMenuScreen.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/ModulesMenuScreen.java new file mode 100644 index 0000000..2d6f2cf --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/ModulesMenuScreen.java @@ -0,0 +1,145 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.clickgui.layout.screen.modules; + +import dev.loki.lovisual.config.i18n.MainConfig; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.modules.model.ModulesMenuCategory; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.modules.model.ModulesMenuStyle; +import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; +import dev.loki.lovisual.features.gui.clickgui.tabs.ClickGuiSearch; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.renderer.warp.RenderWarpStack; +import org.lwjgl.glfw.GLFW; + +import java.util.ArrayList; +import java.util.List; + +public final class ModulesMenuScreen { + static final float INPUT_READY_PROGRESS = 0.72f; + public static final float PANEL_W = 115.0f; + public static final float PANEL_H = 240.0f; + public static final float PANEL_GAP = 14.0f; + public static final float HEADER_H = 24.0f; + public static final float SEPARATOR_H = 4.0f; + public static final float MODULE_ROW_H = 20.0f; + public static final float PANEL_RADIUS = 10.0f; + public static final float TEXT_LEFT_PADDING = 10.0f; + public static final float LIQUID_GLASS_LOGICAL_SCALE = 3.0f; + public static float ACTIVE_PORT_SCALE = LIQUID_GLASS_LOGICAL_SCALE; + + final List panels = new ArrayList<>(); + float areaX; float areaY; float areaW; float areaH; + float scale = 1.0f; float screenAnim = 0.0f; boolean openTarget = false; + float searchAnim = 0.0f; float searchX; float searchY; float searchW; float searchH; + float searchBtnX; float searchBtnY; float searchBtnW; float searchBtnH; float searchBtnHoverAnim; + + public ModulesMenuScreen() { + for (ModulesMenuCategory c : ModulesMenuCategory.values()) panels.add(new ModulesMenuPanel(c, c.ordinal())); + } + + public static float computePortScale() { return ACTIVE_PORT_SCALE; } + static boolean isModuleListEditHeld() { return ModulesMenuLayout.isModuleListEditHeld(); } + static boolean inside(float mx, float my, float x, float y, float w, float h) { return ModulesMenuLayout.inside(mx, my, x, y, w, h); } + + public void open() { openTarget = true; } + public void close() { + openTarget = false; ClickGuiSearch.deactivate(); + if (!ClickGuiRenderer.isClosingForExit()) { screenAnim = 0.0f; ModulesMenuLayout.resetTransientState(panels); } + } + public boolean isVisible() { return openTarget || screenAnim > 0.001f; } + + public void layout(float x, float y, float w, float h) { + this.areaX = x; this.areaY = y; this.areaW = Math.max(1.0f, w); this.areaH = Math.max(1.0f, h); + ModulesMenuLayout.layout(this, panels, areaX, areaY, areaW, areaH); + } + + public void render(float mouseX, float mouseY) { + ModulesMenuChromeRenderer.ensureFonts(); + ModulesMenuStyle.syncTheme(); + float dt = AnimationUtility.deltaTime(); + float target = openTarget ? 1.0f : 0.0f; + screenAnim = AnimationUtility.approach(screenAnim, target, dt, openTarget ? 4.8f : 7.5f); + screenAnim = AnimationUtility.snap(screenAnim, target, 0.01f); + if (screenAnim <= 0.001f && !openTarget) { ModulesMenuLayout.resetTransientState(panels); return; } + searchAnim = AnimationUtility.approach(searchAnim, ClickGuiSearch.isActive() ? 1.0f : 0.0f, dt, 10.0f); + searchAnim = AnimationUtility.snap(searchAnim, ClickGuiSearch.isActive() ? 1.0f : 0.0f, 0.01f); + layout(areaX, areaY, areaW, areaH); + for (ModulesMenuPanel p : panels) { + if (p.isDraggingScrollbar()) p.updateScrollbarDrag(mouseX, mouseY); + float a = ModulesMenuLayout.panelAlpha(p.anim); + if (a <= 0.001f) continue; + try (RenderWarpStack.Scope ignored = ModulesMenuLayout.pushPanelDropWarp(p)) { + ModulesMenuGlassRenderer.renderPanelGlass(p, a, scale); + } + } + for (ModulesMenuPanel p : panels) { + float a = ModulesMenuLayout.panelAlpha(p.anim); + if (a <= 0.001f) continue; + try (RenderWarpStack.Scope ignored = ModulesMenuLayout.pushPanelDropWarp(p)) { + ModulesMenuChromeRenderer.renderPanel(p, mouseX, mouseY, a, scale); + } + } + ModulesMenuChromeRenderer.renderSearchButton(this, mouseX, mouseY, screenAnim, scale); + ModulesMenuChromeRenderer.renderSearch(this, screenAnim, scale); + ModulesMenuChromeRenderer.renderHints(this, screenAnim, scale); + ClickGuiRenderer.flushRenderer(); + } + + public void renderGlassPass(float alphaFactor) { + if (screenAnim <= 0.001f) return; + float passAlpha = AnimationUtility.clamp(alphaFactor, 0.0f, 1.0f); + for (ModulesMenuPanel p : panels) { + float a = ModulesMenuLayout.panelAlpha(p.anim) * passAlpha; + if (a <= 0.001f) continue; + try (RenderWarpStack.Scope ignored = ModulesMenuLayout.pushPanelDropWarp(p)) { + ModulesMenuGlassRenderer.renderPanelGlass(p, a, scale); + } + } + } + + public boolean mousePressed(float mx, float my, int btn) { + if (!ModulesMenuLayout.isInteractive(screenAnim, openTarget)) return false; + if (btn == GLFW.GLFW_MOUSE_BUTTON_LEFT && inside(mx, my, searchBtnX, searchBtnY, searchBtnW, searchBtnH)) { ClickGuiSearch.setActive(!ClickGuiSearch.isActive()); return true; } + if (btn == GLFW.GLFW_MOUSE_BUTTON_LEFT && inside(mx, my, searchX, searchY, searchW, searchH)) { ClickGuiSearch.setActive(true); return true; } + for (ModulesMenuPanel p : panels) if (inside(mx, my, p.x, p.y, p.w, p.h)) return p.mousePressed(mx, my, btn); + if (btn == GLFW.GLFW_MOUSE_BUTTON_LEFT && ClickGuiSearch.isActive()) { ClickGuiSearch.setActive(false); return true; } + return false; + } + + public void mouseReleased(float mx, float my, int btn) { + if (!ModulesMenuLayout.isInteractive(screenAnim, openTarget)) return; + for (ModulesMenuPanel p : panels) p.mouseReleased(mx, my, btn); + } + + public boolean mouseScrolled(float mx, float my, double amt) { + if (!ModulesMenuLayout.isInteractive(screenAnim, openTarget)) return false; + for (ModulesMenuPanel p : panels) if (inside(mx, my, p.x, p.y, p.w, p.h)) { p.scroll(mx, my, amt); return true; } + return false; + } + + public boolean keyPressed(int key, int scan, int mods) { + if (!ModulesMenuLayout.isInteractive(screenAnim, openTarget)) return false; + if (key == GLFW.GLFW_KEY_H && (mods & GLFW.GLFW_MOD_ALT) != 0) { MainConfig c = MainConfig.get(); c.setClickGuiHintsEnabled(!c.isClickGuiHintsEnabled()); return true; } + boolean ctrl = (mods & GLFW.GLFW_MOD_CONTROL) != 0; + if (ctrl && key == GLFW.GLFW_KEY_F) { ClickGuiSearch.setActive(!ClickGuiSearch.isActive()); return true; } + if (ClickGuiSearch.isActive()) { + if (key == GLFW.GLFW_KEY_ESCAPE) { ClickGuiSearch.setActive(false); return true; } + if (key == GLFW.GLFW_KEY_BACKSPACE) { ClickGuiSearch.backspace(); return true; } + } + for (ModulesMenuPanel p : panels) if (p.keyPressed(key, scan, mods)) return true; + return false; + } + + public boolean charTyped(char chr, int mods) { + if (!ModulesMenuLayout.isInteractive(screenAnim, openTarget)) return false; + if (ClickGuiSearch.isActive()) { ClickGuiSearch.append(chr); return true; } + for (ModulesMenuPanel p : panels) if (p.charTyped(chr, mods)) return true; + return false; + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/model/ModulesMenuCategory.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/model/ModulesMenuCategory.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/model/ModulesMenuCategory.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/model/ModulesMenuCategory.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/model/ModulesMenuStyle.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/model/ModulesMenuStyle.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/model/ModulesMenuStyle.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/model/ModulesMenuStyle.java diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/render/ModulesMenuChromeRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/render/ModulesMenuChromeRenderer.java new file mode 100644 index 0000000..0613b21 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/render/ModulesMenuChromeRenderer.java @@ -0,0 +1,178 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.clickgui.layout.screen.modules; + +import dev.loki.lovisual.config.i18n.MainConfig; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.modules.model.ModulesMenuStyle; +import dev.loki.lovisual.features.gui.clickgui.settings.core.Setting; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.module.ModuleComponent; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.LayoutRender2D; +import dev.loki.lovisual.features.gui.clickgui.settings.render.SettingRenderContext; +import dev.loki.lovisual.features.gui.clickgui.settings.render.SettingRenderSurface; +import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; +import dev.loki.lovisual.features.gui.clickgui.tabs.ClickGuiSearch; +import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiI18n; +import dev.loki.lovisual.features.gui.clickgui.util.tooltip.ClickGuiHintOverlay; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.text.font.Fonts; +import dev.loki.lovisual.render.engine.text.font.info.FontInfo; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.render.helpers.clip.ScissorFunction; + +final class ModulesMenuChromeRenderer { + private ModulesMenuChromeRenderer() {} + + static TextRenderer regular; + static TextRenderer medium; + static TextRenderer semibold; + + static void ensureFonts() { + if (regular == null) regular = Fonts.renderer("Onest", FontInfo.Type.Regular, ClickGuiRenderer.getInterRegular()); + if (medium == null) medium = Fonts.renderer("OnestMedium", FontInfo.Type.Regular, regular); + if (semibold == null) semibold = Fonts.renderer("OnestBold", FontInfo.Type.Regular, medium); + } + + static TextRenderer regularForRow() { ensureFonts(); return regular; } + static TextRenderer semiboldForRow() { ensureFonts(); return semibold; } + + static String bindingLabel(ModuleComponent.CardEntry entry) { + String pending = ClickGuiRenderer.getPendingBindDisplay(); + if (pending != null) return pending; + String bind = entry.bindLabel(); + return bind == null || bind.isBlank() ? "Binding..." : "Key: " + bind; + } + + static void renderPanel(ModulesMenuPanel panel, float mouseX, float mouseY, float alpha, float scale) { + panel.update(); + int text = ModulesMenuLayout.withAlpha(ModulesMenuStyle.text(), alpha); + int muted = ModulesMenuLayout.withAlpha(ModulesMenuStyle.textMuted(), alpha); + int split = ModulesMenuLayout.withAlpha(ModulesMenuStyle.split(), alpha); + int veil = ModulesMenuLayout.withAlpha(ModulesMenuStyle.panelBgGlassDark(), alpha * 0.30f); + LayoutRender2D.roundedQuad(panel.x, panel.y, panel.w, panel.h, ModulesMenuScreen.PANEL_RADIUS * scale, veil, veil, veil, veil); + panel.hits.clear(); + panel.settingHits.clear(); + float titleY = panel.y + ModulesMenuLayout.middle(ModulesMenuLayout.textHeight(semibold, 9.0f * scale), ModulesMenuScreen.HEADER_H * scale) + 0.5f * scale; + ClickGuiRenderer.drawText(semibold, panel.category.title(), panel.x + ModulesMenuScreen.TEXT_LEFT_PADDING * scale, titleY, 9.0f * scale, text, false); + ModulesMenuGlassRenderer.drawCategoryIcon(panel, muted, scale); + if (panel.swap < 0.999f) { + ModulesMenuModuleRenderer.renderModulePage(panel, mouseX, mouseY, alpha * (1.0f - panel.swap), scale); + } + if (panel.swap > 0.001f) { + renderSettingsPage(panel, mouseX, mouseY, alpha * panel.swap, scale); + } + LayoutRender2D.rect(panel.x + 8.0f * scale, panel.y + ModulesMenuScreen.HEADER_H * scale, panel.w - 16.0f * scale, 0.5f * scale, split); + } + + static void renderSettingsPage(ModulesMenuPanel panel, float mouseX, float mouseY, float alpha, float scale) { + if (alpha <= 0.001f) return; + float pageX = panel.x + panel.w * (1.0f - panel.swap); + float settingsPadLeft = 6.0f * scale; + float settingsPadRight = 13.0f * scale; + float settingsX = pageX + settingsPadLeft; + float settingsW = Math.max(1.0f, panel.w - settingsPadLeft - settingsPadRight); + float settingsTop = panel.y + (ModulesMenuScreen.HEADER_H + ModulesMenuScreen.SEPARATOR_H) * scale; + float backRowY = panel.y + 28.0f * scale; + float backRowH = 20.0f * scale; + boolean pageClip = ScissorFunction.pushRaw(panel.x, panel.y, panel.w, panel.h); + boolean headerClip = ScissorFunction.pushRaw(panel.x, panel.y, panel.w, panel.h); + if (ModulesMenuLayout.inside(mouseX, mouseY, pageX, backRowY, panel.w, backRowH)) { + int hoverBg = ModulesMenuLayout.withAlpha(ModulesMenuStyle.rowHover(), alpha); + LayoutRender2D.roundedQuad(pageX + 3.0f * scale, backRowY + scale, panel.w - 6.0f * scale, backRowH - 2.0f * scale, 5.0f * scale, hoverBg, hoverBg, hoverBg, hoverBg); + } + String title = panel.selectedTitle == null ? "Settings" : panel.selectedTitle; + title = ClickGuiRenderer.fitText(regular, title, 8.0f * scale, panel.w - 18.0f * scale); + ClickGuiRenderer.drawText(regular, title, pageX + ModulesMenuScreen.TEXT_LEFT_PADDING * scale, + settingsTop + ModulesMenuLayout.middle(ModulesMenuLayout.textHeight(regular, 8.0f * scale), ModulesMenuScreen.HEADER_H * scale) - scale, + 8.0f * scale, ModulesMenuLayout.withAlpha(ModulesMenuStyle.text(), alpha), false); + if (headerClip) ScissorFunction.pop(); + float clipY = settingsTop + ModulesMenuScreen.HEADER_H * scale; + float clipH = panel.h - ModulesMenuScreen.HEADER_H * 2.0f * scale - ModulesMenuScreen.SEPARATOR_H * scale - 0.5f * scale - 5.0f * scale; + boolean clipped = ScissorFunction.pushRaw(settingsX, clipY, settingsW, clipH); + float y = clipY - panel.settingsSmoothScroll; + float total = 0.0f; + try (SettingRenderContext.Scope ignored = SettingRenderContext.push(SettingRenderSurface.MODULES, scale)) { + for (Setting setting : panel.selectedSettings) { + float vis = setting.updateVisibilityAnim(); + if (!setting.isVisibilityTargetVisible() && vis <= 0.01f) continue; + float baseH = setting.getHeight(); + float h = baseH * vis; + if (h > 0.5f) { + panel.settingHits.add(new ModulesMenuPanel.SettingHit(setting, settingsX, y, settingsW, h)); + if (y + h >= clipY && y <= clipY + clipH) { + float slide = (-baseH + baseH * vis) * 0.5f; + boolean settingClipped = ScissorFunction.pushRaw(settingsX, y, settingsW, h); + setting.render(settingsX, y + slide, settingsW, mouseX, mouseY); + if (settingClipped) ScissorFunction.pop(); + } + } + y += h; + total += h; + } + } + panel.maxSettingsScroll = Math.max(0.0f, total - clipH); + panel.settingsScroll = AnimationUtility.clamp(panel.settingsScroll, 0.0f, panel.maxSettingsScroll); + panel.settingsSmoothScroll = AnimationUtility.approach(panel.settingsSmoothScroll, panel.settingsScroll, 0.20f); + panel.settingsSmoothScroll = AnimationUtility.snap(panel.settingsSmoothScroll, panel.settingsScroll, 0.05f); + if (clipped) ScissorFunction.pop(); + ModulesMenuGlassRenderer.renderScrollbar(panel, panel.settingsSmoothScroll, panel.maxSettingsScroll, clipY, clipH, alpha, mouseX, mouseY, scale); + if (pageClip) ScissorFunction.pop(); + } + + static void renderSearchButton(ModulesMenuScreen screen, float mouseX, float mouseY, float screenAnim, float scale) { + float alpha = ModulesMenuLayout.easeOutCubic(screenAnim); + if (alpha <= 0.001f) return; + boolean hovered = ModulesMenuLayout.inside(mouseX, mouseY, screen.searchBtnX, screen.searchBtnY, screen.searchBtnW, screen.searchBtnH); + float dt = AnimationUtility.deltaTime(); + screen.searchBtnHoverAnim = AnimationUtility.approach(screen.searchBtnHoverAnim, hovered ? 1.0f : 0.0f, dt, 12.0f); + screen.searchBtnHoverAnim = AnimationUtility.snap(screen.searchBtnHoverAnim, hovered ? 1.0f : 0.0f, 0.01f); + int bg = ModulesMenuLayout.withAlpha(ModulesMenuStyle.panelBgGlassDark(), alpha * (0.45f + 0.35f * screen.searchBtnHoverAnim)); + LayoutRender2D.roundedQuad(screen.searchBtnX, screen.searchBtnY, screen.searchBtnW, screen.searchBtnH, 6.0f * scale, bg, bg, bg, bg); + LayoutRender2D.roundedStrokeQuad(screen.searchBtnX, screen.searchBtnY, screen.searchBtnW, screen.searchBtnH, 6.0f * scale, 0.5f * scale, + ModulesMenuLayout.withAlpha(ModulesMenuStyle.textMuted(), alpha * 0.35f), + ModulesMenuLayout.withAlpha(ModulesMenuStyle.textMuted(), alpha * 0.35f), + ModulesMenuLayout.withAlpha(ModulesMenuStyle.textMuted(), alpha * 0.35f), + ModulesMenuLayout.withAlpha(ModulesMenuStyle.textMuted(), alpha * 0.35f)); + TextRenderer icons = Fonts.renderer("Icons", FontInfo.Type.Regular, ClickGuiRenderer.getInterRegular()); + float iconSize = 9.0f * scale; + float iconX = screen.searchBtnX + (screen.searchBtnW - ClickGuiRenderer.textWidth(icons, "s", iconSize)) * 0.5f; + float iconY = screen.searchBtnY + ModulesMenuLayout.middle(ModulesMenuLayout.textHeight(icons, iconSize), screen.searchBtnH); + int iconColor = ModulesMenuLayout.withAlpha(ClickGuiSearch.isActive() ? 0xFFF5F5FF : hovered ? 0xFFD0D0DC : 0xFF878894, alpha); + ClickGuiRenderer.drawText(icons, "s", iconX, iconY, iconSize, iconColor, false); + } + + static void renderSearch(ModulesMenuScreen screen, float screenAnim, float scale) { + if (screen.searchAnim <= 0.001f && !ClickGuiSearch.isActive()) return; + float alpha = ModulesMenuLayout.easeOutCubic(screenAnim) * screen.searchAnim; + if (alpha <= 0.001f) return; + float materialAlpha = alpha >= 0.999f ? 1.0f : alpha; + float radius = 6.0f * scale; + ModulesMenuGlassRenderer.drawLiquidGlass(screen.searchX, screen.searchY, screen.searchW, screen.searchH, radius, false, materialAlpha, scale); + int veil = ModulesMenuLayout.withAlpha(ModulesMenuStyle.panelBgGlassDark(), alpha * 0.25f); + LayoutRender2D.roundedQuad(screen.searchX, screen.searchY, screen.searchW, screen.searchH, radius, veil, veil, veil, veil); + String text = ClickGuiSearch.getText(); + String draw = text == null || text.isBlank() ? "Search..." : text; + int color = text == null || text.isBlank() ? ModulesMenuLayout.withAlpha(ModulesMenuStyle.textFaint(), alpha) : ModulesMenuLayout.withAlpha(ModulesMenuStyle.text(), alpha); + ensureFonts(); + ClickGuiRenderer.drawText(regular, draw, screen.searchX + 8.0f * scale, + screen.searchY + ModulesMenuLayout.middle(ModulesMenuLayout.textHeight(regular, 8.0f * scale), screen.searchH), + 8.0f * scale, color, false); + } + + static void renderHints(ModulesMenuScreen screen, float screenAnim, float scale) { + MainConfig config = MainConfig.get(); + if (!config.isClickGuiHintsEnabled()) return; + float alpha = ModulesMenuLayout.easeOutCubic(screenAnim); + ClickGuiHintOverlay.renderBottomLeft(screen.areaX, screen.areaY, screen.areaW, screen.areaH, scale, alpha, + ClickGuiI18n.tr("clickgui.hints.modules.toggle", "LMB - toggle module"), + ClickGuiI18n.tr("clickgui.hints.modules.settings", "RMB - settings"), + ClickGuiI18n.tr("clickgui.hints.modules.module_list", "E - edit ModuleList"), + ClickGuiI18n.tr("clickgui.hints.modules.search", "Ctrl+F - search"), + ClickGuiI18n.tr("clickgui.hints.modules.close", "Esc - close ClickGui"), + ClickGuiI18n.tr("clickgui.hints.modules.hide", "Alt+H - hide hints")); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/render/ModulesMenuGlassRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/render/ModulesMenuGlassRenderer.java new file mode 100644 index 0000000..441539c --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/render/ModulesMenuGlassRenderer.java @@ -0,0 +1,98 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.clickgui.layout.screen.modules; + +import dev.loki.lovisual.features.gui.clickgui.layout.screen.modules.model.ModulesMenuStyle; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.LayoutRender2D; +import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.svg.model.SvgRenderOptions; +import dev.loki.lovisual.render.helpers.util.SystemCursor; + +final class ModulesMenuGlassRenderer { + private ModulesMenuGlassRenderer() {} + + static void renderPanelGlass(ModulesMenuPanel panel, float alpha, float scale) { + if (alpha <= 0.001f) return; + LayoutRender2D.roundedSoftShadow( + panel.x, panel.y, panel.w, panel.h, + ModulesMenuScreen.PANEL_RADIUS * scale, + 18.0f * scale, 0.018f, + ModulesMenuLayout.withAlpha(ModulesMenuStyle.shadow(), alpha)); + float materialAlpha = alpha >= 0.999f ? 1.0f : alpha; + drawLiquidGlass(panel.x, panel.y, panel.w, panel.h, ModulesMenuScreen.PANEL_RADIUS * scale, true, materialAlpha, scale); + } + + static void drawLiquidGlass(float x, float y, float w, float h, float radius, boolean panel, float alpha, float scale) { + if (alpha <= 0.001f) return; + float materialAlpha = AnimationUtility.clamp(alpha, 0.0f, 1.0f); + float blurAlpha = AnimationUtility.clamp(materialAlpha * (panel ? 1.20f : 1.04f), 0.0f, 1.0f); + float thickness = (panel ? 15.5f : 12.5f) * scale; + float fresnelPower = panel ? -18.0f : -16.0f; + float fresnelAlpha = 1.0f; + float baseAlpha = panel ? 0.78f : 0.84f; + float fresnelMix = panel ? 0.52f : 0.48f; + float distortion = (panel ? 0.190f : 0.155f) * materialAlpha; + Renderer2D.COLOR.blurRect(x, y, w, h, radius, ClickGuiRenderer.clickGuiBlurQuality(), 1.0f, blurAlpha, 0xFFFFFF); + Renderer2D.COLOR.liquidGlassRect(x, y, w, h, radius, thickness, 0xFFFFFFFF, + materialAlpha, blurAlpha, fresnelPower, fresnelAlpha, baseAlpha, fresnelMix, distortion, 0.0f); + } + + static void drawCategoryIcon(ModulesMenuPanel panel, int color, float scale) { + float icon = 8.0f * scale; + float ix = panel.x + panel.w - 10.0f * scale - icon; + float iy = panel.y + ModulesMenuLayout.middle(icon, ModulesMenuScreen.HEADER_H * scale) + 0.5f * scale; + Renderer2D.COLOR.svg(panel.category.icon(), ix, iy, icon, icon, SvgRenderOptions.overrideColor(color)); + } + + static void renderScrollbar(ModulesMenuPanel panel, float scroll, float maxScroll, + float clipY, float clipH, float alpha, float mouseX, float mouseY, float scale) { + boolean settingsPage = panel.selected != null; + if (maxScroll <= 0.5f) { + if (settingsPage) { + panel.settingsScrollbarX = panel.settingsScrollbarY = panel.settingsScrollbarW = panel.settingsScrollbarH = 0f; + } else { + panel.modulesScrollbarX = panel.modulesScrollbarY = panel.modulesScrollbarW = panel.modulesScrollbarH = 0f; + } + return; + } + float trackW = 2.5f * scale; + float trackX = panel.x + panel.w - 5.0f * scale; + float trackY = clipY + 5.0f * scale; + float trackH = Math.max(1.0f, clipH - 10.0f * scale); + int trackA = ModulesMenuStyle.scrollTrackA(alpha); + int trackB = ModulesMenuStyle.scrollTrackB(alpha); + int handleA = ModulesMenuStyle.scrollHandleA(alpha); + int handleB = ModulesMenuStyle.scrollHandleB(alpha); + boolean scrollbarHovered = ModulesMenuLayout.inside(mouseX, mouseY, trackX - 3.0f * scale, trackY, trackW + 6.0f * scale, trackH); + boolean scrollbarDragging = settingsPage ? panel.settingsScrollbarDragging : panel.modulesScrollbarDragging; + if (scrollbarHovered || scrollbarDragging) { + SystemCursor.set(SystemCursor.CursorType.SCROLL); + } + LayoutRender2D.roundedQuad(trackX, trackY, trackW, trackH, trackW * 0.5f, trackA, trackB, trackB, trackA); + float handleH = Math.max(18.0f * scale, trackH * (trackH / (trackH + maxScroll))); + float handleY = trackY + (trackH - handleH) * (scroll / Math.max(1.0f, maxScroll)); + if (settingsPage) { + panel.settingsScrollbarX = trackX; + panel.settingsScrollbarY = trackY; + panel.settingsScrollbarW = trackW; + panel.settingsScrollbarH = trackH; + panel.settingsScrollbarHandleY = handleY; + panel.settingsScrollbarHandleH = handleH; + } else { + panel.modulesScrollbarX = trackX; + panel.modulesScrollbarY = trackY; + panel.modulesScrollbarW = trackW; + panel.modulesScrollbarH = trackH; + panel.modulesScrollbarHandleY = handleY; + panel.modulesScrollbarHandleH = handleH; + } + LayoutRender2D.roundedQuad(trackX, handleY, trackW, handleH, trackW * 0.5f, handleA, handleB, handleB, handleA); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/render/ModulesMenuModuleRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/render/ModulesMenuModuleRenderer.java new file mode 100644 index 0000000..6a1f5c5 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/render/ModulesMenuModuleRenderer.java @@ -0,0 +1,138 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.clickgui.layout.screen.modules; + +import dev.loki.lovisual.features.gui.clickgui.layout.screen.modules.model.ModulesMenuStyle; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.module.ModuleComponent; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.LayoutRender2D; +import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; +import dev.loki.lovisual.features.gui.clickgui.tabs.ClickGuiSearch; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.svg.model.SvgRenderOptions; +import dev.loki.lovisual.render.helpers.clip.ClipFunction; +import dev.loki.lovisual.util.logging.DebugLog; + +import java.util.List; + +final class ModulesMenuModuleRenderer { + private ModulesMenuModuleRenderer() {} + + static void renderModulePage(ModulesMenuPanel panel, float mouseX, float mouseY, float alpha, float scale) { + if (alpha <= 0.001f) return; + float pageX = panel.x - panel.w * panel.swap; + float listY = panel.y + (ModulesMenuScreen.HEADER_H + ModulesMenuScreen.SEPARATOR_H - 1.0f) * scale; + float clipY = panel.y + (ModulesMenuScreen.HEADER_H + ModulesMenuScreen.SEPARATOR_H) * scale; + float clipH = panel.h - (ModulesMenuScreen.HEADER_H + ModulesMenuScreen.SEPARATOR_H) * scale - 0.5f * scale; + boolean clipped = ClipFunction.pushRaw(panel.x, clipY, panel.w, clipH); + ClickGuiRenderer.flushRenderer(); + boolean panelShapeClip = ClipFunction.pushRoundedRect(panel.x, panel.y, panel.w, panel.h, ModulesMenuScreen.PANEL_RADIUS * scale); + float listW = Math.max(1.0f, panel.w - 7.0f * scale); + float y = listY - panel.modulesSmoothScroll; + float total = 0.0f; + try { + List entries = ModulesMenuResolver.buildCards(panel.category); + for (ModuleComponent.CardEntry entry : entries) { + if (!ClickGuiSearch.matches(entry.title(), entry.searchAliases())) continue; + if (y + ModulesMenuScreen.MODULE_ROW_H * scale >= clipY && y <= clipY + clipH) { + renderModuleRow(panel, entry, pageX, y, listW, mouseX, mouseY, alpha, scale); + } + y += ModulesMenuScreen.MODULE_ROW_H * scale; + total += ModulesMenuScreen.MODULE_ROW_H * scale; + } + } finally { + ClickGuiRenderer.flushRenderer(); + if (panelShapeClip) ClipFunction.pop(); + } + panel.maxModulesScroll = Math.max(0.0f, total - clipH); + panel.modulesScroll = AnimationUtility.clamp(panel.modulesScroll, 0.0f, panel.maxModulesScroll); + panel.modulesSmoothScroll = AnimationUtility.approach(panel.modulesSmoothScroll, panel.modulesScroll, 0.22f); + panel.modulesSmoothScroll = AnimationUtility.snap(panel.modulesSmoothScroll, panel.modulesScroll, 0.05f); + if (clipped) ClipFunction.pop(); + ModulesMenuGlassRenderer.renderScrollbar(panel, panel.modulesSmoothScroll, panel.maxModulesScroll, clipY, clipH, alpha, mouseX, mouseY, scale); + } + + static void renderModuleRow(ModulesMenuPanel panel, ModuleComponent.CardEntry entry, + float x, float y, float rowW, float mouseX, float mouseY, float alpha, float scale) { + float h = ModulesMenuScreen.MODULE_ROW_H * scale; + boolean hover = ModulesMenuLayout.inside(mouseX, mouseY, x, y, rowW, h); + float enabled = panel.enabledAnim(entry.getId(), entry.enabled()); + float hoverAnim = panel.hoverAnim(entry.getId(), hover); + renderModuleRowHover(panel, x, y, rowW, h, mouseX, mouseY, alpha, hoverAnim, scale); + String label = panel.bindingId != null && panel.bindingId.equals(entry.getId()) + ? ModulesMenuChromeRenderer.bindingLabel(entry) : entry.title(); + boolean moduleListEdit = ModulesMenuLayout.isModuleListEditHeld(); + String moduleListLabel = entry.shownInModuleList() ? "[ON]" : "[OFF]"; + float moduleListTextSize = 7.2f * scale; + float moduleListTextW = moduleListEdit ? ClickGuiRenderer.textWidth(ModulesMenuChromeRenderer.semiboldForRow(), moduleListLabel, moduleListTextSize) : 0.0f; + float moduleListW = moduleListEdit ? moduleListTextW + 3.0f * scale : 0.0f; + float checkReserve = entry.enabled() ? 18.0f * scale : 6.0f * scale; + float moduleListX = x + rowW - checkReserve - moduleListW - 3.0f * scale; + float moduleListY = y + ModulesMenuLayout.middle(ModulesMenuLayout.textHeight(ModulesMenuChromeRenderer.semiboldForRow(), moduleListTextSize), h) - 0.5f * scale; + int nameColor = ModulesMenuLayout.mix(ModulesMenuLayout.withAlpha(ModulesMenuStyle.textMuted(), alpha), ModulesMenuLayout.withAlpha(ModulesMenuStyle.text(), alpha), 0.25f + 0.75f * enabled + 0.20f * hoverAnim); + float nameX = x + (ModulesMenuScreen.TEXT_LEFT_PADDING + 2.0f * enabled) * scale; + float nameY = y + ModulesMenuLayout.middle(ModulesMenuLayout.textHeight(ModulesMenuChromeRenderer.regularForRow(), 8.0f * scale), h) - 0.5f * scale; + float nameMaxW = moduleListEdit ? Math.max(12.0f * scale, moduleListX - nameX - 5.0f * scale) + : Math.max(12.0f * scale, rowW - (nameX - x) - 18.0f * scale); + String visibleLabel = ClickGuiRenderer.fitText(ModulesMenuChromeRenderer.regularForRow(), label, 8.0f * scale, nameMaxW); + ClickGuiRenderer.drawText(ModulesMenuChromeRenderer.regularForRow(), visibleLabel, nameX, nameY, 8.0f * scale, nameColor, false); + if (moduleListEdit) { + int listColor = entry.shownInModuleList() ? ModulesMenuStyle.MODULE_LIST_ON : ModulesMenuStyle.MODULE_LIST_OFF; + ClickGuiRenderer.drawText(ModulesMenuChromeRenderer.semiboldForRow(), moduleListLabel, moduleListX, moduleListY, moduleListTextSize, ModulesMenuLayout.withAlpha(listColor, alpha), false); + } + int divider = ModulesMenuLayout.withAlpha(ModulesMenuStyle.text(), alpha * 0.02f); + LayoutRender2D.rect(x + 3.0f * scale, y + h, Math.max(1f, rowW - 6.0f * scale), 0.5f * scale, divider); + if (enabled > 0.001f) { + float check = 6.0f * scale; + float checkX = x + rowW - 15.0f * scale - 2.0f * scale * enabled; + float checkY = y + 7.0f * scale; + Renderer2D.COLOR.svg("check", checkX, checkY, check, check, + SvgRenderOptions.overrideColor(ModulesMenuLayout.withAlpha(ModulesMenuStyle.text(), alpha * (0.10f + 0.90f * enabled)))); + } + panel.hits.add(new ModulesMenuPanel.ModuleHit(entry.getId(), x, y, rowW, h, entry.hasSettings(), entry.toggleable())); + } + + static void renderModuleRowHover(ModulesMenuPanel panel, float x, float y, float w, float h, + float mouseX, float mouseY, float alpha, float hoverAnim, float scale) { + if (hoverAnim <= 0.001f || alpha <= 0.001f) return; + float rx = x + 3.0f * scale; + float ry = y + 2.0f * scale; + float rw = w - 6.0f * scale; + float rh = h - 4.0f * scale; + if (rw <= 0.5f || rh <= 0.5f) return; + float radius = 5.0f * scale; + int hoverBg = ModulesMenuLayout.withAlpha(ModulesMenuStyle.rowHover(), alpha * hoverAnim); + int glow = ModulesMenuStyle.rowHoverGlow(alpha * hoverAnim); + float cx = AnimationUtility.clamp(mouseX, rx, rx + rw); + float cy = AnimationUtility.clamp(mouseY, ry, ry + rh); + float glowRadius = Math.max(rw, rh) * 0.82f; + boolean ownsPanelClip = false; + boolean panelClip = ClipFunction.isShapeClipActive(); + if (!panelClip) { + panelClip = ClipFunction.pushRoundedRect(panel.x, panel.y, panel.w, panel.h, ModulesMenuScreen.PANEL_RADIUS * scale); + ownsPanelClip = panelClip; + } + if (!panelClip) { + DebugLog.warnOnChange("modules.hover.parent.clip.failed", panel.category.title(), + "ModulesMenuScreen: hover clipped draw skipped because parent rounded glass clip failed. panel=%s panelBounds=[%.2f, %.2f, %.2f, %.2f]", + panel.category.title(), panel.x, panel.y, panel.w, panel.h); + return; + } + try { + if (((glow >>> 24) & 0xFF) > 0) { + Renderer2D.COLOR.radialGlowMasked(rx, ry, rw, rh, radius, 0.0f, glowRadius, cx, cy, glow); + } + if (((hoverBg >>> 24) & 0xFF) > 0) { + LayoutRender2D.roundedQuad(rx, ry, rw, rh, radius, hoverBg, hoverBg, hoverBg, hoverBg); + } + } finally { + Renderer2D.flushBatch(Renderer2D.FlushReason.SCISSOR); + if (ownsPanelClip) ClipFunction.pop(); + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/state/ModulesMenuLayout.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/state/ModulesMenuLayout.java new file mode 100644 index 0000000..40ac6c5 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/state/ModulesMenuLayout.java @@ -0,0 +1,142 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.clickgui.layout.screen.modules; + +import dev.loki.lovisual.features.gui.clickgui.layout.screen.modules.model.ModulesMenuCategory; +import dev.loki.lovisual.features.gui.clickgui.tabs.ClickGuiSearch; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.renderer.warp.RenderWarpStack; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; +import net.minecraft.client.Minecraft; +import org.lwjgl.glfw.GLFW; + +import java.util.List; + +final class ModulesMenuLayout { + private ModulesMenuLayout() {} + + static float easeOutCubic(float t) { + t = AnimationUtility.clamp(t, 0.0f, 1.0f); + float inv = 1.0f - t; + return 1.0f - inv * inv * inv; + } + + static float middle(float child, float parent) { return (parent - child) * 0.5f; } + + static float textHeight(TextRenderer renderer, float size) { return ClickGuiRenderer.textHeight(renderer, size); } + + static boolean inside(float mx, float my, float x, float y, float w, float h) { + return mx >= x && mx <= x + w && my >= y && my <= y + h; + } + + static int withAlpha(int color, float alpha) { + float a = AnimationUtility.clamp(alpha, 0.0f, 1.0f); + int ca = (color >>> 24) & 0xFF; + int na = Math.round(ca * a); + return (color & 0x00FFFFFF) | ((na & 0xFF) << 24); + } + + static int withAlpha(int color, int alpha) { + int a = Math.max(0, Math.min(255, alpha)); + return (color & 0x00FFFFFF) | (a << 24); + } + + static int mix(int from, int to, float t) { + t = AnimationUtility.clamp(t, 0.0f, 1.0f); + int a = Math.round(((from >>> 24) & 0xFF) * (1.0f - t) + ((to >>> 24) & 0xFF) * t); + int r = Math.round(((from >>> 16) & 0xFF) * (1.0f - t) + ((to >>> 16) & 0xFF) * t); + int g = Math.round(((from >>> 8) & 0xFF) * (1.0f - t) + ((to >>> 8) & 0xFF) * t); + int b = Math.round((from & 0xFF) * (1.0f - t) + (to & 0xFF) * t); + return ((a & 0xFF) << 24) | ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (b & 0xFF); + } + + static boolean isModuleListEditHeld() { + Minecraft client = Minecraft.getInstance(); + if (client == null || client.getWindow() == null || ClickGuiSearch.isActive()) return false; + return GLFW.glfwGetKey(client.getWindow().handle(), GLFW.GLFW_KEY_E) == GLFW.GLFW_PRESS; + } + + static float panelProgress(float screenAnim, int panelCount, int index) { + if (panelCount == 0) return screenAnim; + float stagger = 0.055f; + float span = Math.max(0.20f, 1.0f - stagger * Math.max(0, panelCount - 1)); + return AnimationUtility.clamp((screenAnim - index * stagger) / span, 0.0f, 1.0f); + } + + static float panelAlpha(float anim) { return easeOutCubic(anim); } + + static boolean isInteractive(float screenAnim, boolean openTarget) { + return openTarget && screenAnim >= ModulesMenuScreen.INPUT_READY_PROGRESS; + } + + static RenderWarpStack.Scope pushPanelDropWarp(ModulesMenuPanel panel) { + float t = easeOutCubic(panel.anim); + if (t >= 0.999f) return Renderer2D.pushWarp(null); + float inv = 1.0f - t; + return Renderer2D.pushPerspectiveWarp(panel.x, panel.y, panel.w, panel.h, 0.0f, -9.0f * inv, 0.0f, 3.4f, 0.82f, 0.94f + 0.06f * t); + } + + static void resetTransientState(List panels) { + for (ModulesMenuPanel p : panels) { + p.selected = null; p.selectedTitle = null; p.selectedSettings.clear(); + p.settingsScroll = 0.0f; p.settingsSmoothScroll = 0.0f; + p.modulesScroll = 0.0f; p.modulesSmoothScroll = 0.0f; p.swap = 0.0f; + p.hits.clear(); p.settingHits.clear(); + } + } + + static float computePortScale(float areaW, float areaH) { + float designW = ModulesMenuScreen.PANEL_W * ModulesMenuCategory.values().length + + ModulesMenuScreen.PANEL_GAP * (ModulesMenuCategory.values().length - 1); + float sideMargin = Math.max(28.0f, areaW * 0.03f); + float verticalReserve = Math.max(56.0f, areaH * 0.10f); + float fitX = Math.max(1.0f, (areaW - sideMargin * 2.0f) / designW); + float fitY = Math.max(1.0f, (areaH - verticalReserve) / (ModulesMenuScreen.PANEL_H + 30.0f)); + float fit = Math.min(fitX, fitY); + return AnimationUtility.clamp(Math.min(ModulesMenuScreen.LIQUID_GLASS_LOGICAL_SCALE, fit), 2.55f, ModulesMenuScreen.LIQUID_GLASS_LOGICAL_SCALE); + } + + static void layout(ModulesMenuScreen screen, List panels, float areaX, float areaY, float areaW, float areaH) { + float scale = computePortScale(areaW, areaH); + screen.scale = scale; + ModulesMenuScreen.ACTIVE_PORT_SCALE = scale; + float pw = ModulesMenuScreen.PANEL_W * scale; + float ph = ModulesMenuScreen.PANEL_H * scale; + float gap = ModulesMenuScreen.PANEL_GAP * scale; + float total = (pw + gap) * panels.size() - gap; + float outerPadX = 18.0f * scale; + float outerPadTop = 6.0f * scale; + float outerPadBottom = 26.0f * scale; + float layoutW = Math.max(total, areaW - outerPadX * 2.0f); + float startX = areaX + (layoutW - total) * 0.5f + (areaW - layoutW) * 0.5f; + startX = Math.max(areaX + outerPadX, startX); + float py = areaY + outerPadTop + Math.max(0.0f, (areaH - outerPadTop - outerPadBottom - ph) * 0.5f); + for (int i = 0; i < panels.size(); i++) { + ModulesMenuPanel panel = panels.get(i); + float targetX = startX + i * (pw + gap); + float panelT = panelProgress(screen.screenAnim, panels.size(), i); + float eased = easeOutCubic(panelT); + float drop = -(38.0f + i * 3.5f) * scale * (1.0f - eased); + float settle = (float) Math.sin(panelT * Math.PI) * 4.5f * scale; + panel.x = targetX; panel.y = py + drop + settle; panel.w = pw; panel.h = ph; panel.anim = panelT; + } + screen.searchW = 100.0f * scale; screen.searchH = 20.0f * scale; + screen.searchX = areaX + areaW * 0.5f - screen.searchW * 0.5f; + float panelBottom = py + ph; + float visibleSearchY = panelBottom + 6.0f * scale; + float maxVisibleSearchY = areaY + areaH - screen.searchH - 8.0f * scale; + if (visibleSearchY > maxVisibleSearchY) visibleSearchY = maxVisibleSearchY; + float hiddenSearchY = visibleSearchY + 12.0f * scale; + screen.searchY = AnimationUtility.lerp(hiddenSearchY, visibleSearchY, screen.searchAnim); + screen.searchBtnW = 22.0f * scale; screen.searchBtnH = 22.0f * scale; + screen.searchBtnX = areaX + areaW - screen.searchBtnW - 14.0f * scale; + screen.searchBtnY = areaY + 12.0f * scale; + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/MenuScreen.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/MenuScreen.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/MenuScreen.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/MenuScreen.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/MenuSettingsResolver.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/MenuSettingsResolver.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/MenuSettingsResolver.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/MenuSettingsResolver.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/SettingsGuiPalette.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/SettingsGuiPalette.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/SettingsGuiPalette.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/SettingsGuiPalette.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/category/CategoryComponent.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/category/CategoryComponent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/category/CategoryComponent.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/category/CategoryComponent.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/ConfigDiffPanelComponent.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/ConfigDiffPanelComponent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/ConfigDiffPanelComponent.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/ConfigDiffPanelComponent.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/ConfigProfileCardComponent.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/ConfigProfileCardComponent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/ConfigProfileCardComponent.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/ConfigProfileCardComponent.java diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/ConfigProfilesComponent.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/ConfigProfilesComponent.java new file mode 100644 index 0000000..228f8f7 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/ConfigProfilesComponent.java @@ -0,0 +1,125 @@ +package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.config; + +import dev.loki.lovisual.config.profile.codec.ConfigProfileType; +import dev.loki.lovisual.config.profile.service.ConfigProfileService; +import dev.loki.lovisual.config.profile.snapshot.ConfigProfileMeta; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; +import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiMath; +import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; +import org.lwjgl.glfw.GLFW; + +import java.nio.file.Path; +import java.util.List; + +public final class ConfigProfilesComponent { + private final ConfigProfileCardComponent cardComponent = new ConfigProfileCardComponent(); + private final ConfigDiffPanelComponent diffPanelComponent = new ConfigDiffPanelComponent(); + private final ConfigProfilesState state = new ConfigProfilesState(); + private final ConfigProfilesActions actions = new ConfigProfilesActions(state, diffPanelComponent); + private final ConfigProfilesToolbarRenderer toolbar = new ConfigProfilesToolbarRenderer(state); + private final ConfigProfilesListRenderer listRenderer = new ConfigProfilesListRenderer(state, cardComponent); + + private static boolean inside(ConfigProfileCardComponent.ActionHit hit, float mx, float my) { + return hit != null && ClickGuiMath.insideRect(mx, my, hit.x(), hit.y(), hit.w(), hit.h()); + } + + public void resetScroll() { + state.resetScroll(); + diffPanelComponent.resetScroll(); + } + + public void render(float menuX, float menuY, float menuW, float menuH, float mx, float my, float scale) { + state.hits().clear(); + SettingsGuiPalette palette = SettingsGuiPalette.current(); + float areaX = menuX + 31f * scale; + float areaY = menuY + 33f * scale; + float areaW = menuW - 42f * scale; + float areaH = menuH - 39f * scale; + toolbar.render(areaX, areaY, areaW, mx, my, scale, palette); + float listX = areaX; + float listY = areaY + 24f * scale; + float listW = areaW; + float listH = areaH - 24f * scale; + List entries = actions.filteredProfiles(ConfigProfileService.INSTANCE.list(state.type())); + listRenderer.renderCards(listX, listY, listW, listH, entries, mx, my, scale, palette); + if (state.selectedDiff() != null || state.diffError() != null || diffPanelComponent.isActive()) { + diffPanelComponent.render(menuX + menuW + 24f * scale, menuY + 4f * scale, 160f * scale, + Math.min(200f * scale, menuH - 8f * scale), state.selected(), state.selectedDiff(), state.diffError(), scale, palette); + if (!diffPanelComponent.isOpen() && !diffPanelComponent.isActive()) { + state.selectedDiff(null); + state.diffError(null); + } + } + listRenderer.renderStatus(areaX, areaY, areaW, scale, palette); + } + + public boolean mousePressedScrollbar(float mx, float my, int button) { + if (button != 0) return false; + if (diffPanelComponent.mousePressedScrollbar(mx, my, button)) return true; + if (!listRenderer.isScrollbarHovered(mx, my)) return false; + state.draggingScrollbar(true); + state.scrollbarDragOffset(ClickGuiMath.insideRect(mx, my, state.scrollbarX(), state.scrollbarThumbY(), state.scrollbarW(), state.scrollbarThumbH()) + ? my - state.scrollbarThumbY() : state.scrollbarThumbH() * 0.5f); + listRenderer.scrollToMouse(my); + return true; + } + + public void mouseReleased(int button) { + diffPanelComponent.mouseReleased(button); + if (button == 0) state.draggingScrollbar(false); + } + + public void scroll(float mx, float my, float menuX, float menuY, float menuW, float menuH, double amount, float scale) { + if (diffPanelComponent.scroll(mx, my, amount)) return; + float x = menuX + 31f * scale; + float y = menuY + 55f * scale; + float w = menuW - 42f * scale; + float h = menuH - 61f * scale; + if (!ClickGuiMath.insideRect(mx, my, x, y, w, h)) return; + state.scroll(state.scroll() + (float) (amount * 22f)); + } + + public boolean click(float mx, float my, int button) { + if (button != 0) return false; + if (state.selectedDiff() != null || state.diffError() != null) { + ConfigDiffPanelComponent.HeaderAction diffAction = diffPanelComponent.headerAction(mx, my, button); + if (diffAction == ConfigDiffPanelComponent.HeaderAction.CLOSE) { diffPanelComponent.close(); return true; } + if (diffAction == ConfigDiffPanelComponent.HeaderAction.SAVE) { actions.overwriteSelected(); return true; } + } + if (toolbar.modulesPill().contains(mx, my)) { actions.switchType(ConfigProfileType.MODULES); return true; } + if (toolbar.hudPill().contains(mx, my)) { actions.switchType(ConfigProfileType.HUD); return true; } + if (toolbar.themesPill().contains(mx, my)) { actions.switchType(ConfigProfileType.THEMES); return true; } + if (toolbar.saveButton().contains(mx, my)) { toolbar.pulseSave(); actions.saveCurrent(); return true; } + if (listRenderer.isScrollbarHovered(mx, my)) return true; + for (ConfigProfilesState.CardEntryHit entryHit : state.hits()) { + var hit = entryHit.hit(); + if (inside(hit.title(), mx, my)) { actions.select(entryHit.meta()); state.beginRename(entryHit.meta(), hit.title().x(), hit.title().y(), hit.title().w(), hit.title().h()); return true; } + if (inside(hit.diff(), mx, my)) { actions.select(entryHit.meta()); diffPanelComponent.resetScroll(); actions.loadDiff(entryHit.meta()); return true; } + if (inside(hit.apply(), mx, my)) { actions.select(entryHit.meta()); actions.apply(entryHit.meta()); return true; } + if (inside(hit.delete(), mx, my)) { actions.delete(entryHit.meta()); return true; } + if (ClickGuiMath.insideRect(mx, my, hit.x(), hit.y(), hit.w(), hit.h())) { actions.select(entryHit.meta()); return true; } + } + return false; + } + + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (!state.isRenaming()) return false; + if (keyCode == GLFW.GLFW_KEY_ESCAPE) { state.cancelRename(); return true; } + if (keyCode == GLFW.GLFW_KEY_ENTER || keyCode == GLFW.GLFW_KEY_KP_ENTER) { actions.commitRename(); return true; } + if (keyCode == GLFW.GLFW_KEY_BACKSPACE) { + if (!state.editingText().isEmpty()) state.editingText(state.editingText().substring(0, state.editingText().length() - 1)); + return true; + } + return true; + } + + public boolean charTyped(char chr, int modifiers) { + if (!state.isRenaming()) return false; + if (chr >= 32 && chr != 127 && state.editingText().length() < 48) state.editingText(state.editingText() + chr); + return true; + } + + public boolean importDroppedFiles(List paths) { + return actions.importDroppedFiles(paths); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/render/ConfigProfilesListRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/render/ConfigProfilesListRenderer.java new file mode 100644 index 0000000..98b999f --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/render/ConfigProfilesListRenderer.java @@ -0,0 +1,149 @@ +package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.config; + +import dev.loki.lovisual.config.profile.snapshot.ConfigProfileMeta; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.LayoutRender2D; +import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; +import dev.loki.lovisual.features.gui.clickgui.tabs.ClickGuiSearch; +import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiI18n; +import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiMath; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.helpers.clip.ScissorFunction; +import dev.loki.lovisual.render.helpers.util.SystemCursor; + +import java.util.List; + +final class ConfigProfilesListRenderer { + private final ConfigProfilesState state; + private final ConfigProfileCardComponent cardComponent; + + ConfigProfilesListRenderer(ConfigProfilesState state, ConfigProfileCardComponent cardComponent) { + this.state = state; + this.cardComponent = cardComponent; + } + + void renderCards(float x, float y, float w, float h, List entries, float mx, float my, float scale, SettingsGuiPalette palette) { + float gap = 6f * scale; + int columns = w > 230f * scale ? 2 : 1; + float cardW = (w - gap * (columns - 1)) / columns; + float cardH = 58f * scale; + float stepY = cardH + gap; + int rows = (entries.size() + columns - 1) / columns; + float contentH = rows <= 0 ? 0f : rows * stepY - gap; + float maxScroll = Math.max(0f, contentH - h); + updateScrollbarMetrics(x, y, w, h, maxScroll, scale); + if (state.draggingScrollbar()) scrollToMouse(ClickGuiRenderer.getMouseY()); + + state.scroll(ClickGuiMath.clamp(state.scroll(), -maxScroll, 0f)); + state.smoothedScroll(AnimationUtility.approach(state.smoothedScroll(), state.scroll(), state.draggingScrollbar() ? 0.55f : 0.2f)); + state.smoothedScroll(AnimationUtility.snap(state.smoothedScroll(), state.scroll(), state.draggingScrollbar() ? 0.01f : 0.05f)); + + if (entries.isEmpty()) { + String empty = ClickGuiSearch.isActive() && !ClickGuiSearch.getText().isBlank() + ? ClickGuiI18n.tr("clickgui.settings.config.empty.no_matches", "No matching configs.") + : ClickGuiI18n.tr("clickgui.settings.config.empty.none_saved", "No saved configs."); + float emptySize = 8.0f * scale; + float emptyW = ClickGuiRenderer.textWidth(ClickGuiRenderer.getInterRegular(), empty, emptySize); + ClickGuiRenderer.drawText(ClickGuiRenderer.getInterRegular(), empty, x + (w - emptyW) * 0.5f, y + h * 0.42f, emptySize, palette.panelMuted(), false); + if (!ClickGuiSearch.isActive() || ClickGuiSearch.getText().isBlank()) { + String hint = ClickGuiI18n.tr("clickgui.settings.config.empty.drop_hint", "Drop .lvcfg files here, or put them anywhere under config/lovisual/profiles/."); + float hintSize = 6.9f * scale; + String fitted = ClickGuiRenderer.fitText(ClickGuiRenderer.getInterRegular(), hint, hintSize, w - 18f * scale); + float hintW = ClickGuiRenderer.textWidth(ClickGuiRenderer.getInterRegular(), fitted, hintSize); + ClickGuiRenderer.drawText(ClickGuiRenderer.getInterRegular(), fitted, x + (w - hintW) * 0.5f, y + h * 0.42f + 13f * scale, hintSize, palette.panelMuted(), false); + } + return; + } + boolean clipped = ScissorFunction.pushRaw(x, y, w, h); + for (int i = 0; i < entries.size(); i++) { + int col = i % columns; + int row = i / columns; + float cx = x + col * (cardW + gap); + float cy = y + row * stepY + state.smoothedScroll(); + if (cy + cardH < y || cy > y + h) continue; + ConfigProfileMeta meta = entries.get(i); + boolean selectedState = state.selected() != null && state.selected().getId().equals(meta.getId()) && state.selected().type() == meta.type(); + var hit = cardComponent.render(meta, cx, cy, cardW, cardH, mx, my, selectedState, scale, palette); + if (state.isEditing(meta)) { + state.editingTitleRect(new ConfigProfilesState.Rect(hit.title().x(), hit.title().y(), hit.title().w(), hit.title().h())); + renderRenameInput(state.editingTitleRect(), scale, palette); + } + if (hit.title() != null && ClickGuiMath.insideRect(mx, my, hit.title().x(), hit.title().y(), hit.title().w(), hit.title().h())) { + SystemCursor.set(SystemCursor.CursorType.MOVE); + } + state.hits().add(new ConfigProfilesState.CardEntryHit(meta, hit)); + } + if (clipped) ScissorFunction.pop(); + renderScrollbar(x, y, w, h, maxScroll, scale, palette); + } + + void renderStatus(float x, float y, float w, float scale, SettingsGuiPalette palette) { + if (!state.isStatusVisible()) return; + float size = 6.8f * scale; + String text = ClickGuiRenderer.fitText(ClickGuiRenderer.getInterRegular(), state.statusMessage(), size, w - 176f * scale); + float tw = ClickGuiRenderer.textWidth(ClickGuiRenderer.getInterRegular(), text, size); + ClickGuiRenderer.drawText(ClickGuiRenderer.getInterRegular(), text, x + w - tw, y + 7.2f * scale, size, palette.panelMuted(), false); + } + + private void renderRenameInput(ConfigProfilesState.Rect rect, float scale, SettingsGuiPalette palette) { + float padX = 2f * scale; + float yy = rect.y() - scale; + float hh = rect.h() + 2f * scale; + LayoutRender2D.roundedQuad(rect.x() - padX, yy, rect.w() + padX * 2f, hh, 3f * scale, + SettingsGuiPalette.mix(palette.moduleCardTop(), palette.menuCategorySelectedLeft(), 0.28f), + SettingsGuiPalette.mix(palette.moduleCardTopStrong(), palette.menuCategorySelectedRight(), 0.22f), + SettingsGuiPalette.mix(palette.moduleCardTopStrong(), palette.menuCategorySelectedRight(), 0.22f), + SettingsGuiPalette.mix(palette.moduleCardTop(), palette.menuCategorySelectedLeft(), 0.28f)); + String text = ClickGuiRenderer.fitText(ClickGuiRenderer.getInterMedium(), state.editingText(), 8.8f * scale, rect.w() - 5f * scale); + ClickGuiRenderer.drawText(ClickGuiRenderer.getInterMedium(), text, rect.x(), rect.y(), 8.8f * scale, palette.moduleTitleText(), false); + if ((System.currentTimeMillis() / 500L) % 2L == 0L) { + float tw = ClickGuiRenderer.textWidth(ClickGuiRenderer.getInterMedium(), text, 8.8f * scale); + LayoutRender2D.rect(rect.x() + Math.min(tw + 1.5f * scale, rect.w() - 2f * scale), rect.y() + 1.2f * scale, 0.6f * scale, 8f * scale, palette.moduleTitleText()); + } + } + + void renderScrollbar(float x, float y, float w, float h, float maxScroll, float scale, SettingsGuiPalette palette) { + updateScrollbarMetrics(x, y, w, h, maxScroll, scale); + if (!state.scrollbarVisible()) return; + float mx = ClickGuiRenderer.getMouseX(); + float my = ClickGuiRenderer.getMouseY(); + if (isScrollbarHovered(mx, my) || state.draggingScrollbar()) SystemCursor.set(SystemCursor.CursorType.SCROLL); + LayoutRender2D.roundedQuad(state.scrollbarX(), state.scrollbarY(), state.scrollbarW(), state.scrollbarH(), 2f * scale, + palette.moduleScrollTrackA(), palette.moduleScrollTrackB(), palette.moduleScrollTrackB(), palette.moduleScrollTrackA()); + LayoutRender2D.roundedQuad(state.scrollbarX(), state.scrollbarThumbY(), state.scrollbarW(), state.scrollbarThumbH(), 2f * scale, + palette.moduleScrollHandleA(), palette.moduleScrollHandleB(), palette.moduleScrollHandleB(), palette.moduleScrollHandleA()); + } + + void updateScrollbarMetrics(float x, float y, float w, float h, float maxScroll, float scale) { + boolean visible = maxScroll > 0.5f; + state.scrollbarVisible(visible); + state.scrollbarMaxScroll(Math.max(0f, maxScroll)); + if (!visible) { + state.draggingScrollbar(false); + state.scrollbarX(0f); state.scrollbarY(0f); state.scrollbarW(0f); state.scrollbarH(0f); state.scrollbarThumbY(0f); state.scrollbarThumbH(0f); + return; + } + state.scrollbarW(3f * scale); + state.scrollbarX(x + w - state.scrollbarW() - 2f * scale); + state.scrollbarY(y + 3f * scale); + state.scrollbarH(Math.max(1f, h - 6f * scale)); + state.scrollbarThumbH(Math.max(18f * scale, state.scrollbarH() * (state.scrollbarH() / (state.scrollbarMaxScroll() + state.scrollbarH())))); + float ratio = state.scrollbarMaxScroll() <= 0f ? 0f : (-state.smoothedScroll() / state.scrollbarMaxScroll()); + state.scrollbarThumbY(state.scrollbarY() + (state.scrollbarH() - state.scrollbarThumbH()) * AnimationUtility.clamp(ratio, 0f, 1f)); + } + + boolean isScrollbarHovered(float mx, float my) { + if (!state.scrollbarVisible()) return false; + float pad = 4f; + return ClickGuiMath.insideRect(mx, my, state.scrollbarX() - pad, state.scrollbarY() - pad, state.scrollbarW() + pad * 2f, state.scrollbarH() + pad * 2f); + } + + void scrollToMouse(float my) { + if (!state.scrollbarVisible() || state.scrollbarMaxScroll() <= 0f) return; + float span = state.scrollbarH() - state.scrollbarThumbH(); + if (span <= 0.5f) return; + float thumbTop = AnimationUtility.clamp(my - state.scrollbarDragOffset(), state.scrollbarY(), state.scrollbarY() + span); + float ratio = (thumbTop - state.scrollbarY()) / span; + state.scroll((float) (-state.scrollbarMaxScroll() * AnimationUtility.clamp(ratio, 0f, 1f))); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/render/ConfigProfilesToolbarRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/render/ConfigProfilesToolbarRenderer.java new file mode 100644 index 0000000..52da229 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/render/ConfigProfilesToolbarRenderer.java @@ -0,0 +1,159 @@ +package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.config; + +import dev.loki.lovisual.config.profile.codec.ConfigProfileType; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.LayoutRender2D; +import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; +import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiI18n; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.svg.model.SvgRenderOptions; +import dev.loki.lovisual.render.helpers.util.SystemCursor; + +final class ConfigProfilesToolbarRenderer { + private final ConfigProfilesState state; + + private ConfigProfilesState.Rect modulesPill = ConfigProfilesState.Rect.ZERO; + private ConfigProfilesState.Rect hudPill = ConfigProfilesState.Rect.ZERO; + private ConfigProfilesState.Rect themesPill = ConfigProfilesState.Rect.ZERO; + private ConfigProfilesState.Rect saveButton = ConfigProfilesState.Rect.ZERO; + + private float modeAnim; + private float modulesHoverAnim; + private float hudHoverAnim; + private float themesHoverAnim; + private float saveHoverAnim; + private float savePulseAnim; + + ConfigProfilesToolbarRenderer(ConfigProfilesState state) { + this.state = state; + } + + ConfigProfilesState.Rect modulesPill() { return modulesPill; } + ConfigProfilesState.Rect hudPill() { return hudPill; } + ConfigProfilesState.Rect themesPill() { return themesPill; } + ConfigProfilesState.Rect saveButton() { return saveButton; } + + void pulseSave() { savePulseAnim = 1f; } + + void render(float x, float y, float w, float mx, float my, float scale, SettingsGuiPalette palette) { + float pillW = 132f * scale; + float pillH = 15f * scale; + float targetMode = switch (state.type()) { + case MODULES -> 0f; + case HUD -> 1f; + case THEMES -> 2f; + }; + float dt = AnimationUtility.deltaTime(); + modeAnim = AnimationUtility.approach(modeAnim, targetMode, dt, 12f); + modeAnim = AnimationUtility.snap(modeAnim, targetMode, 0.01f); + + modulesPill = new ConfigProfilesState.Rect(x, y, pillW / 3f, pillH); + hudPill = new ConfigProfilesState.Rect(x + pillW / 3f, y, pillW / 3f, pillH); + themesPill = new ConfigProfilesState.Rect(x + pillW * 2f / 3f, y, pillW / 3f, pillH); + + modulesHoverAnim = updateHover(modulesHoverAnim, modulesPill.contains(mx, my), dt); + hudHoverAnim = updateHover(hudHoverAnim, hudPill.contains(mx, my), dt); + themesHoverAnim = updateHover(themesHoverAnim, themesPill.contains(mx, my), dt); + + int baseLeft = SettingsGuiPalette.mix(palette.panelPillBase(), palette.panelBgLeft(), 0.34f); + int baseRight = SettingsGuiPalette.mix(palette.panelPillBase(), palette.panelBgRight(), 0.44f); + float anyHover = Math.max(modulesHoverAnim, Math.max(hudHoverAnim, themesHoverAnim)); + baseLeft = SettingsGuiPalette.mix(baseLeft, palette.menuCategoryHoverLeft(), 0.12f * anyHover); + baseRight = SettingsGuiPalette.mix(baseRight, palette.menuCategoryHoverRight(), 0.12f * anyHover); + + ClickGuiRenderer.drawBlur(x, y, pillW, pillH, 4f * scale, palette.panelBlurTint(), 145f / 255f); + LayoutRender2D.roundedQuad(x, y, pillW, pillH, 3.5f * scale, baseLeft, baseRight, baseRight, baseLeft); + LayoutRender2D.roundedStrokeQuad(x, y, pillW, pillH, 3.5f * scale, 0.5f * scale, + SettingsGuiPalette.mix(palette.panelStroke(), palette.moduleDividerStart(), 0.18f), + SettingsGuiPalette.mix(palette.panelStroke(), palette.moduleDividerEnd(), 0.18f), + SettingsGuiPalette.mix(palette.panelStroke(), palette.moduleDividerEnd(), 0.18f), + SettingsGuiPalette.mix(palette.panelStroke(), palette.moduleDividerStart(), 0.18f)); + + float pad = 1.2f * scale; + float segment = (pillW - pad * 2f) / 3f; + float activeX = x + pad + segment * modeAnim; + float activeHover = switch (state.type()) { + case MODULES -> modulesHoverAnim; + case HUD -> hudHoverAnim; + case THEMES -> themesHoverAnim; + }; + int activeA = SettingsGuiPalette.mix(palette.panelPillActive(), palette.menuCategorySelectedLeft(), 0.22f + activeHover * 0.08f); + int activeB = SettingsGuiPalette.mix(palette.panelPillActive(), palette.menuCategorySelectedRight(), 0.22f + activeHover * 0.12f); + int activeC = SettingsGuiPalette.mix(SettingsGuiPalette.darken(palette.panelPillActive(), 0.16f), palette.menuCategorySelectedRight(), 0.18f + activeHover * 0.10f); + LayoutRender2D.roundedQuad(activeX, y + pad, segment, pillH - pad * 2f, 3f * scale, activeA, activeB, activeC, activeA); + LayoutRender2D.roundedStrokeQuad(activeX, y + pad, segment, pillH - pad * 2f, 3f * scale, 0.35f * scale, + SettingsGuiPalette.withAlpha(palette.moduleDividerStart(), 150), + SettingsGuiPalette.withAlpha(palette.moduleDividerEnd(), 178), + SettingsGuiPalette.withAlpha(palette.moduleDividerEnd(), 142), + SettingsGuiPalette.withAlpha(palette.moduleDividerStart(), 150)); + + drawToolbarSeparator(x + pad + segment, y, pillH, scale, palette); + drawToolbarSeparator(x + pad + segment * 2f, y, pillH, scale, palette); + + drawSegment(modulesPill, ClickGuiI18n.tr("clickgui.settings.config.type.modules", "Modules"), state.type() == ConfigProfileType.MODULES, modulesHoverAnim, scale, palette); + drawSegment(hudPill, ClickGuiI18n.tr("clickgui.settings.config.type.hud", "Hud"), state.type() == ConfigProfileType.HUD, hudHoverAnim, scale, palette); + drawSegment(themesPill, ClickGuiI18n.tr("clickgui.settings.config.type.themes", "Themes"), state.type() == ConfigProfileType.THEMES, themesHoverAnim, scale, palette); + + float saveW = 68f * scale; + saveButton = new ConfigProfilesState.Rect(x + pillW + 7f * scale, y, saveW, pillH); + boolean saveHover = saveButton.contains(mx, my); + saveHoverAnim = updateHover(saveHoverAnim, saveHover, dt); + savePulseAnim = AnimationUtility.snap(AnimationUtility.approach(savePulseAnim, 0f, dt, 7f), 0f, 0.01f); + renderSaveButton(saveButton, saveHoverAnim, savePulseAnim, scale, palette); + if (saveHover) SystemCursor.set(SystemCursor.CursorType.MOVE); + } + + private float updateHover(float current, boolean hovered, float dt) { + return AnimationUtility.snap(AnimationUtility.approach(current, hovered ? 1f : 0f, dt, 12f), hovered ? 1f : 0f, 0.01f); + } + + private void drawToolbarSeparator(float x, float y, float h, float scale, SettingsGuiPalette palette) { + float backingW = Math.max(1.25f * scale, 1.05f); + float backingH = h - 4.4f * scale; + float backingY = y + (h - backingH) * 0.5f; + int backingTop = SettingsGuiPalette.withAlpha(palette.panelBgLeft(), 98); + int backingBottom = SettingsGuiPalette.withAlpha(palette.panelBgRight(), 78); + LayoutRender2D.roundedQuad(x - backingW * 0.5f, backingY, backingW, backingH, backingW * 0.5f, backingTop, backingTop, backingBottom, backingBottom); + float lineW = Math.max(0.5f * scale, 0.75f); + float lineH = h - 6.0f * scale; + float lineY = y + (h - lineH) * 0.5f; + int top = SettingsGuiPalette.withAlpha(SettingsGuiPalette.mix(palette.moduleDividerStart(), palette.menuCategorySelectedLeft(), 0.34f), 146); + int mid = SettingsGuiPalette.withAlpha(SettingsGuiPalette.mix(palette.moduleDividerEnd(), palette.menuCategorySelectedRight(), 0.40f), 210); + int bottom = SettingsGuiPalette.withAlpha(SettingsGuiPalette.mix(palette.moduleDividerEnd(), palette.panelText(), 0.24f), 136); + LayoutRender2D.roundedQuad(x - lineW * 0.5f, lineY, lineW, lineH, lineW * 0.5f, top, mid, bottom, top); + } + + private void renderSaveButton(ConfigProfilesState.Rect rect, float hover, float pulse, float scale, SettingsGuiPalette palette) { + float lift = (hover * 0.65f + pulse * 0.85f) * scale; + float x = rect.x(); + float y = rect.y() - lift * 0.15f; + float w = rect.w(); + float h = rect.h(); + int bgA = SettingsGuiPalette.mix(palette.panelPillBase(), palette.menuCategoryHoverLeft(), 0.18f + hover * 0.22f + pulse * 0.24f); + int bgB = SettingsGuiPalette.mix(palette.panelPillBase(), palette.menuCategoryHoverRight(), 0.16f + hover * 0.24f + pulse * 0.26f); + int bgC = SettingsGuiPalette.mix(SettingsGuiPalette.darken(palette.panelPillBase(), 0.18f), palette.menuCategorySelectedRight(), 0.12f + hover * 0.18f + pulse * 0.24f); + LayoutRender2D.roundedSoftShadow(x, y + 0.4f * scale, w, h, 3.6f * scale, 5f * scale, 0.02f + hover * 0.03f, + SettingsGuiPalette.withAlpha(palette.menuShadow(), Math.round(65f + hover * 50f + pulse * 58f))); + LayoutRender2D.roundedQuad(x, y, w, h, 3.6f * scale, bgA, bgB, bgC, bgA); + LayoutRender2D.roundedStrokeQuad(x, y, w, h, 3.6f * scale, (0.45f + hover * 0.22f + pulse * 0.22f) * scale, + SettingsGuiPalette.withAlpha(palette.moduleDividerStart(), Math.round(138f + hover * 45f + pulse * 50f)), + SettingsGuiPalette.withAlpha(palette.moduleDividerEnd(), Math.round(158f + hover * 55f + pulse * 55f)), + SettingsGuiPalette.withAlpha(palette.moduleDividerEnd(), Math.round(120f + hover * 45f + pulse * 50f)), + SettingsGuiPalette.withAlpha(palette.moduleDividerStart(), Math.round(138f + hover * 45f + pulse * 50f))); + int iconColor = SettingsGuiPalette.mix(palette.panelText(), palette.menuCategoryText(), 0.22f + hover * 0.34f + pulse * 0.34f); + Renderer2D.COLOR.svg("save", x + 6.2f * scale, y + 4f * scale, 7f * scale, 7f * scale, SvgRenderOptions.overrideColor(iconColor)); + String label = ClickGuiI18n.tr("clickgui.settings.config.save", "Save"); + float textSize = 6.7f * scale; + int textColor = SettingsGuiPalette.mix(palette.panelText(), palette.menuCategoryText(), hover * 0.24f + pulse * 0.24f); + ClickGuiRenderer.drawText(ClickGuiRenderer.getInterMedium(), label, x + 17.4f * scale, y + 4.2f * scale, textSize, textColor, false); + } + + private void drawSegment(ConfigProfilesState.Rect rect, String label, boolean active, float hover, float scale, SettingsGuiPalette palette) { + float size = 7.5f * scale; + float tw = ClickGuiRenderer.textWidth(ClickGuiRenderer.getInterMedium(), label, size); + float th = ClickGuiRenderer.textHeight(ClickGuiRenderer.getInterMedium(), size); + int color = active ? palette.panelText() : SettingsGuiPalette.mix(palette.panelMuted(), palette.panelText(), 0.20f * hover); + ClickGuiRenderer.drawText(ClickGuiRenderer.getInterMedium(), label, rect.x() + (rect.w() - tw) * 0.5f, rect.y() + (rect.h() - th) * 0.5f, size, color, false); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/state/ConfigProfilesActions.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/state/ConfigProfilesActions.java new file mode 100644 index 0000000..4223889 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/state/ConfigProfilesActions.java @@ -0,0 +1,195 @@ +package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.config; + +import dev.loki.lovisual.config.profile.codec.ConfigProfileType; +import dev.loki.lovisual.config.profile.service.ConfigProfileApplier; +import dev.loki.lovisual.config.profile.service.ConfigProfileService; +import dev.loki.lovisual.config.profile.service.ConfigProfileStorage; +import dev.loki.lovisual.config.profile.snapshot.ConfigProfileMeta; +import dev.loki.lovisual.features.gui.clickgui.tabs.ClickGuiSearch; +import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiI18n; +import dev.loki.lovisual.util.logging.DebugLog; + +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +final class ConfigProfilesActions { + private static final DateTimeFormatter GENERATED_NAME_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH-mm-ss"); + + private final ConfigProfilesState state; + private final ConfigDiffPanelComponent diffPanel; + + ConfigProfilesActions(ConfigProfilesState state, ConfigDiffPanelComponent diffPanel) { + this.state = state; + this.diffPanel = diffPanel; + } + + List filteredProfiles(List src) { + if (!ClickGuiSearch.isActive() || ClickGuiSearch.getText().isBlank()) return src; + List out = new ArrayList<>(); + String q = ClickGuiSearch.getText().toLowerCase(Locale.ROOT); + for (ConfigProfileMeta meta : src) { + if (meta.name().toLowerCase(Locale.ROOT).contains(q) || meta.author().toLowerCase(Locale.ROOT).contains(q)) out.add(meta); + } + return out; + } + + void switchType(ConfigProfileType next) { + if (state.type() == next) return; + state.type(next); + state.selected(null); + state.selectedDiff(null); + state.diffError(null); + diffPanel.close(); + state.clearStatus(); + state.cancelRename(); + state.resetScroll(); + diffPanel.resetScroll(); + } + + void select(ConfigProfileMeta meta) { + ConfigProfileMeta sel = state.selected(); + boolean same = sel != null && meta != null && sel.type() == meta.type() && sel.getId().equals(meta.getId()); + state.selected(meta); + if (!same) { + state.selectedDiff(null); + state.diffError(null); + diffPanel.close(); + diffPanel.resetScroll(); + } + } + + void saveCurrent() { + String name = state.type().folderName() + " " + GENERATED_NAME_TIME.format(LocalDateTime.now()); + try { + diffPanel.close(); + ConfigProfileMeta saved = ConfigProfileService.INSTANCE.saveCurrent(state.type(), name).meta(); + state.selected(saved); + state.selectedDiff(null); + state.diffError(null); + state.setStatus(ClickGuiI18n.tr("clickgui.settings.config.status.saved", "Saved: %s", saved.name())); + } catch (Exception e) { + state.setStatus(ClickGuiI18n.tr("clickgui.settings.config.status.save_failed", "Save failed")); + DebugLog.error("Failed to save config profile", e); + } + } + + void overwriteSelected() { + if (state.selected() == null) return; + try { + diffPanel.close(); + ConfigProfileMeta saved = ConfigProfileService.INSTANCE.overwriteCurrent(state.selected()).meta(); + state.selected(saved); + state.selectedDiff(null); + state.diffError(null); + diffPanel.resetScroll(); + state.setStatus(ClickGuiI18n.tr("clickgui.settings.config.status.overwritten", "Overwritten: %s", saved.name())); + } catch (Exception e) { + state.setStatus(ClickGuiI18n.tr("clickgui.settings.config.status.overwrite_failed", "Overwrite failed")); + DebugLog.error("Failed to overwrite config profile", e); + } + } + + void loadDiff(ConfigProfileMeta meta) { + try { + var diff = ConfigProfileService.INSTANCE.diffWithCurrent(meta.type(), meta.getId()); + state.selectedDiff(diff); + state.diffError(null); + diffPanel.open(); + diffPanel.resetScroll(); + state.setStatus(diff.isEmpty() + ? ClickGuiI18n.tr("clickgui.settings.config.status.no_differences", "No differences") + : ClickGuiI18n.tr("clickgui.settings.config.status.diff_count", "Diff: %s changes", diff.valueChangeCount())); + } catch (Exception e) { + state.selectedDiff(null); + state.diffError(e.getMessage()); + diffPanel.open(); + diffPanel.resetScroll(); + state.setStatus(ClickGuiI18n.tr("clickgui.settings.config.status.diff_failed", "Diff failed")); + DebugLog.error("Failed to diff config profile", e); + } + } + + void apply(ConfigProfileMeta meta) { + try { + diffPanel.close(); + ConfigProfileApplier.ApplyResult result = ConfigProfileService.INSTANCE.apply(meta.type(), meta.getId()); + state.selectedDiff(null); + state.diffError(null); + state.setStatus(ClickGuiI18n.tr("clickgui.settings.config.status.applied", "Applied: %s, missing: %s/%s", result.appliedOwners(), result.missingOwners(), result.missingValues())); + } catch (Exception e) { + state.setStatus(ClickGuiI18n.tr("clickgui.settings.config.status.apply_failed", "Apply failed")); + DebugLog.error("Failed to apply config profile", e); + } + } + + void delete(ConfigProfileMeta meta) { + try { + ConfigProfileService.INSTANCE.delete(meta.type(), meta.getId()); + ConfigProfileMeta sel = state.selected(); + if (sel != null && sel.getId().equals(meta.getId()) && sel.type() == meta.type()) { + state.selected(null); + state.selectedDiff(null); + state.diffError(null); + diffPanel.close(); + } + state.setStatus(ClickGuiI18n.tr("clickgui.settings.config.status.deleted", "Deleted: %s", meta.name())); + } catch (Exception e) { + state.setStatus(ClickGuiI18n.tr("clickgui.settings.config.status.delete_failed", "Delete failed")); + DebugLog.error("Failed to delete config profile", e); + } + } + + boolean importDroppedFiles(List paths) { + if (paths == null || paths.isEmpty()) return false; + int imported = 0; + int failed = 0; + ConfigProfileMeta last = null; + for (Path path : paths) { + try { + ConfigProfileStorage.ImportResult result = ConfigProfileService.INSTANCE.importProfileFile(path, false); + imported++; + last = result.meta(); + } catch (Exception e) { + failed++; + DebugLog.error("Failed to import dropped config profile %s", e, path == null ? "" : path.toAbsolutePath()); + } + } + if (last != null) { + switchType(last.type()); + state.selected(last); + } + if (imported > 0 && failed > 0) { + state.setStatus(ClickGuiI18n.tr("clickgui.settings.config.status.imported_partial", "Imported: %s, failed: %s", imported, failed)); + } else if (imported > 0) { + state.setStatus(ClickGuiI18n.tr("clickgui.settings.config.status.imported", "Imported: %s", imported)); + } else { + state.setStatus(ClickGuiI18n.tr("clickgui.settings.config.status.import_failed", "Import failed")); + } + return true; + } + + void commitRename() { + if (!state.isRenaming()) return; + String next = state.editingText() == null ? "" : state.editingText().trim(); + if (next.isBlank()) { + state.setStatus(ClickGuiI18n.tr("clickgui.settings.config.status.empty_name", "Name cannot be empty")); + return; + } + try { + ConfigProfileMeta oldSelected = state.selected(); + ConfigProfileMeta renamed = ConfigProfileService.INSTANCE.rename(state.editingType(), state.editingId(), next).meta(); + if (oldSelected != null && oldSelected.type() == state.editingType() && oldSelected.getId().equals(state.editingId())) { + state.selected(renamed); + } + state.cancelRename(); + state.setStatus(ClickGuiI18n.tr("clickgui.settings.config.status.renamed", "Renamed: %s", renamed.name())); + } catch (Exception e) { + state.setStatus(ClickGuiI18n.tr("clickgui.settings.config.status.rename_failed", "Rename failed")); + DebugLog.error("Failed to rename config profile", e); + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/state/ConfigProfilesState.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/state/ConfigProfilesState.java new file mode 100644 index 0000000..6e714cf --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/config/state/ConfigProfilesState.java @@ -0,0 +1,149 @@ +package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.config; + +import dev.loki.lovisual.config.profile.codec.ConfigProfileType; +import dev.loki.lovisual.config.profile.diff.ConfigProfileDiff; +import dev.loki.lovisual.config.profile.snapshot.ConfigProfileMeta; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.config.ConfigProfileCardComponent.CardHit; +import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiMath; + +import java.util.ArrayList; +import java.util.List; + +final class ConfigProfilesState { + private static final long STATUS_DURATION_MS = 2400L; + + private ConfigProfileType type = ConfigProfileType.MODULES; + private ConfigProfileMeta selected; + private ConfigProfileDiff selectedDiff; + private String diffError; + private String statusMessage; + private long statusUntilMs; + + private ConfigProfileType editingType; + private String editingId; + private String editingText = ""; + private Rect editingTitleRect = Rect.ZERO; + + private float scroll; + private float smoothedScroll; + private boolean scrollbarVisible; + private boolean draggingScrollbar; + private float scrollbarX; + private float scrollbarY; + private float scrollbarW; + private float scrollbarH; + private float scrollbarThumbY; + private float scrollbarThumbH; + private float scrollbarMaxScroll; + + private float scrollbarDragOffset; + private final List hits = new ArrayList<>(); + + ConfigProfileType type() { return type; } + void type(ConfigProfileType v) { type = v; } + + ConfigProfileMeta selected() { return selected; } + void selected(ConfigProfileMeta v) { selected = v; } + + ConfigProfileDiff selectedDiff() { return selectedDiff; } + void selectedDiff(ConfigProfileDiff v) { selectedDiff = v; } + + String diffError() { return diffError; } + void diffError(String v) { diffError = v; } + + String statusMessage() { return statusMessage; } + long statusUntilMs() { return statusUntilMs; } + + ConfigProfileType editingType() { return editingType; } + String editingId() { return editingId; } + String editingText() { return editingText; } + void editingText(String v) { editingText = v; } + Rect editingTitleRect() { return editingTitleRect; } + void editingTitleRect(Rect v) { editingTitleRect = v; } + + float scroll() { return scroll; } + void scroll(float v) { scroll = v; } + float smoothedScroll() { return smoothedScroll; } + void smoothedScroll(float v) { smoothedScroll = v; } + + boolean scrollbarVisible() { return scrollbarVisible; } + void scrollbarVisible(boolean v) { scrollbarVisible = v; } + boolean draggingScrollbar() { return draggingScrollbar; } + void draggingScrollbar(boolean v) { draggingScrollbar = v; } + float scrollbarX() { return scrollbarX; } + void scrollbarX(float v) { scrollbarX = v; } + float scrollbarY() { return scrollbarY; } + void scrollbarY(float v) { scrollbarY = v; } + float scrollbarW() { return scrollbarW; } + float scrollbarW(float v) { scrollbarW = v; return v; } + float scrollbarH() { return scrollbarH; } + void scrollbarH(float v) { scrollbarH = v; } + float scrollbarThumbY() { return scrollbarThumbY; } + void scrollbarThumbY(float v) { scrollbarThumbY = v; } + float scrollbarThumbH() { return scrollbarThumbH; } + void scrollbarThumbH(float v) { scrollbarThumbH = v; } + float scrollbarMaxScroll() { return scrollbarMaxScroll; } + void scrollbarMaxScroll(float v) { scrollbarMaxScroll = v; } + float scrollbarDragOffset() { return scrollbarDragOffset; } + void scrollbarDragOffset(float v) { scrollbarDragOffset = v; } + + List hits() { return hits; } + + void resetScroll() { + scroll = 0f; + smoothedScroll = 0f; + draggingScrollbar = false; + } + + void setStatus(String message) { + statusMessage = message; + statusUntilMs = System.currentTimeMillis() + STATUS_DURATION_MS; + } + + void clearStatus() { + statusMessage = null; + statusUntilMs = 0L; + } + + boolean isStatusVisible() { + if (statusMessage == null || statusMessage.isBlank()) return false; + if (System.currentTimeMillis() > statusUntilMs) { + clearStatus(); + return false; + } + return true; + } + + boolean isRenaming() { + return editingType != null && editingId != null; + } + + boolean isEditing(ConfigProfileMeta meta) { + return meta != null && editingType == meta.type() && editingId != null && editingId.equals(meta.getId()); + } + + void beginRename(ConfigProfileMeta meta, float x, float y, float w, float h) { + if (meta == null) return; + editingType = meta.type(); + editingId = meta.getId(); + editingText = meta.name(); + editingTitleRect = new Rect(x, y, w, h); + clearStatus(); + } + + void cancelRename() { + editingType = null; + editingId = null; + editingText = ""; + editingTitleRect = Rect.ZERO; + } + + record CardEntryHit(ConfigProfileMeta meta, CardHit hit) {} + + record Rect(float x, float y, float w, float h) { + static final Rect ZERO = new Rect(0f, 0f, 0f, 0f); + boolean contains(float mx, float my) { + return ClickGuiMath.insideRect(mx, my, x, y, w, h); + } + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/main/MainSettingsComponent.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/main/MainSettingsComponent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/main/MainSettingsComponent.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/main/MainSettingsComponent.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/ModuleComponent.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/ModuleComponent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/ModuleComponent.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/ModuleComponent.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/ModuleSettingsWindow.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/ModuleSettingsWindow.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/ModuleSettingsWindow.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/ModuleSettingsWindow.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/SettingsPanelComponent.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/SettingsPanelComponent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/SettingsPanelComponent.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/SettingsPanelComponent.java diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/panel/SettingsPanelChrome.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/panel/SettingsPanelChrome.java new file mode 100644 index 0000000..00f9ab5 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/panel/SettingsPanelChrome.java @@ -0,0 +1,173 @@ +package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.module; + +import dev.loki.lovisual.render.engine.svg.model.SvgRenderOptions; +import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.LayoutRender2D; +import dev.loki.lovisual.features.gui.clickgui.settings.core.Setting; +import dev.loki.lovisual.features.gui.clickgui.util.tooltip.SettingTooltip; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.helpers.util.SystemCursor; + +/** + * Отрисовка панели настроек: матовое хром-окно, кнопка-крестик, + * HUD-пилюля режимов, скроллбар, тултип строки. Чистый draw — состояние + * (геометрия/анимации) читает из {@link SettingsPanelComponent}. + */ +final class SettingsPanelChrome { + private static final float DROPDOWN_RADIUS = 10.0f; + + private SettingsPanelChrome() {} + + static boolean inside(float mx, float my, float x, float y, float w, float h) { + return mx >= x && mx <= x + w && my >= y && my <= y + h; + } + + static int mixColor(int from, int to, float t) { + t = Math.max(0f, Math.min(1f, t)); + int a = (int) (((from >>> 24) & 0xFF) * (1 - t) + ((to >>> 24) & 0xFF) * t); + int r = (int) (((from >>> 16) & 0xFF) * (1 - t) + ((to >>> 16) & 0xFF) * t); + int g = (int) (((from >>> 8) & 0xFF) * (1 - t) + ((to >>> 8) & 0xFF) * t); + int b = (int) (((from) & 0xFF) * (1 - t) + ((to) & 0xFF) * t); + return (a << 24) | (r << 16) | (g << 8) | b; + } + + static void renderSettingTooltip(SettingsPanelComponent p, float mx, float my, float panelScale) { + for (SettingsPanelRows.SettingHit hit : p.rows.hits()) { + if (inside(mx, my, hit.x(), hit.y(), hit.w(), hit.h())) { + SettingTooltip.render(hit.setting(), mx, my, hit.y() + hit.h(), panelScale, SettingsGuiPalette.current()); + return; + } + } + } + + static void renderMatteChrome(SettingsPanelComponent p, String header, float scale, int bgTl, int bgTr, + int bgBr, int bgBl, int stroke, int line, int text, int muted) { + if (p.panelW <= 0.5f || p.panelH <= 0.5f || p.openAnim <= 0.001f) return; + SettingsGuiPalette palette = SettingsGuiPalette.current(); + + float radius = DROPDOWN_RADIUS * scale; + float headerBottom = p.panelY + SettingsPanelComponent.DROPDOWN_HEADER_H * scale; + int shadow = LayoutRender2D.alpha(palette.panelShadow(), 0.72f * p.openAnim); + int blurTint = LayoutRender2D.alpha(palette.panelBlurTint(), p.openAnim); + int topWashA = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelBgLeft(), palette.menuCategorySelectedLeft(), 0.16f), p.openAnim); + int topWashB = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelBgRight(), palette.menuCategorySelectedRight(), 0.18f), p.openAnim); + int bodyA = LayoutRender2D.alpha(SettingsGuiPalette.mix(bgTl, palette.menuWindowBgLeft(), 0.18f), p.openAnim); + int bodyB = LayoutRender2D.alpha(SettingsGuiPalette.mix(bgTr, palette.menuWindowBgRight(), 0.18f), p.openAnim); + int bodyC = LayoutRender2D.alpha(SettingsGuiPalette.darken(bgBr, 0.06f), p.openAnim); + int accent = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelPillActive(), palette.menuCategorySelectedRight(), 0.18f), 0.55f * p.openAnim); + int accentSoft = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelPillActive(), palette.panelBgRight(), 0.42f), 0.22f * p.openAnim); + int strokeTop = LayoutRender2D.alpha(SettingsGuiPalette.mix(stroke, palette.moduleDividerEnd(), 0.22f), p.openAnim); + int strokeBottom = LayoutRender2D.alpha(SettingsGuiPalette.mix(stroke, palette.panelBgRight(), 0.22f), 0.82f * p.openAnim); + + LayoutRender2D.roundedSoftShadow(p.panelX - 1.4f * scale, p.panelY + 1.2f * scale, p.panelW + 2.8f * scale, p.panelH + 2.4f * scale, radius + 1.6f * scale, 9.5f * scale, 0.028f, shadow); + ClickGuiRenderer.drawBlur(p.panelX, p.panelY, p.panelW, p.panelH, radius, blurTint, (150f / 255f) * p.openAnim); + LayoutRender2D.roundedQuad(p.panelX, p.panelY, p.panelW, p.panelH, radius, bodyA, bodyB, bodyC, bgBl); + LayoutRender2D.roundedStrokeQuad(p.panelX, p.panelY, p.panelW, p.panelH, radius, 0.58f * scale, strokeTop, LayoutRender2D.alpha(palette.moduleDividerEnd(), p.openAnim * 0.72f), strokeBottom, LayoutRender2D.alpha(strokeTop, 0.82f)); + + LayoutRender2D.roundedQuad(p.panelX + 1.0f * scale, p.panelY + 1.0f * scale, p.panelW - 2.0f * scale, SettingsPanelComponent.DROPDOWN_HEADER_H * scale - 1.0f * scale, Math.max(0f, radius - 1.0f * scale), topWashA, topWashB, LayoutRender2D.alpha(SettingsGuiPalette.darken(topWashB, 0.18f), 0.64f), LayoutRender2D.alpha(SettingsGuiPalette.darken(topWashA, 0.14f), 0.64f)); + LayoutRender2D.rectQuad(p.panelX + 8.0f * scale, headerBottom, p.panelW - 16.0f * scale, Math.max(0.45f, 0.55f * scale), LayoutRender2D.alpha(line, 0.55f), line, LayoutRender2D.alpha(line, 0.28f), LayoutRender2D.alpha(line, 0.28f)); + LayoutRender2D.rectQuad(p.panelX + 8.0f * scale, headerBottom + 0.6f * scale, p.panelW - 16.0f * scale, Math.max(0.45f, 0.65f * scale), accent, accentSoft, 0x00000000, 0x00000000); + + float markW = 2.4f * scale; + float markH = 9.2f * scale; + LayoutRender2D.roundedQuad(p.panelX + 8.0f * scale, p.panelY + 7.2f * scale, markW, markH, markW * 0.5f, accent, accentSoft, accentSoft, accent); + ClickGuiRenderer.drawText(ClickGuiRenderer.getInterMedium(), header, p.panelX + 14.0f * scale, p.panelY + 7.0f * scale, 8.0f * scale, text, false); + + renderCloseButton(p, scale, text, muted, palette); + } + + private static void renderCloseButton(SettingsPanelComponent p, float scale, int text, int muted, SettingsGuiPalette palette) { + int bgA = SettingsGuiPalette.mix(palette.panelPillBase(), palette.menuCategoryHoverLeft(), 0.16f + p.closeHoverAnim * 0.26f); + int bgB = SettingsGuiPalette.mix(palette.panelPillBase(), palette.menuCategoryHoverRight(), 0.18f + p.closeHoverAnim * 0.28f); + int bgC = SettingsGuiPalette.mix(SettingsGuiPalette.darken(palette.panelBgRight(), 0.06f), palette.menuCategoryHoverRight(), 0.12f + p.closeHoverAnim * 0.20f); + int strokeA = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelStroke(), palette.moduleDividerStart(), 0.26f), p.openAnim * (0.72f + p.closeHoverAnim * 0.22f)); + int strokeB = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.moduleDividerEnd(), palette.menuCategorySelectedRight(), 0.22f), p.openAnim * (0.78f + p.closeHoverAnim * 0.20f)); + float radius = p.closeH * 0.50f; + + ClickGuiRenderer.drawBlur(p.closeX, p.closeY, p.closeW, p.closeH, radius, palette.panelBlurTint(), (72f / 255f) * p.openAnim); + LayoutRender2D.roundedSoftShadow(p.closeX, p.closeY + 0.4f * scale, p.closeW, p.closeH, radius, 4.0f * scale, 0.018f + p.closeHoverAnim * 0.018f, LayoutRender2D.alpha(palette.menuShadow(), p.openAnim * (0.38f + p.closeHoverAnim * 0.36f))); + LayoutRender2D.roundedQuad(p.closeX, p.closeY, p.closeW, p.closeH, radius, LayoutRender2D.alpha(bgA, p.openAnim), LayoutRender2D.alpha(bgB, p.openAnim), LayoutRender2D.alpha(bgC, p.openAnim), LayoutRender2D.alpha(bgA, p.openAnim)); + LayoutRender2D.roundedStrokeQuad(p.closeX, p.closeY, p.closeW, p.closeH, radius, 0.42f * scale, strokeA, strokeB, LayoutRender2D.alpha(strokeB, 0.74f), LayoutRender2D.alpha(strokeA, 0.78f)); + + float icon = 6.8f * scale; + int iconColor = LayoutRender2D.alpha(SettingsGuiPalette.mix(muted, text, 0.42f + p.closeHoverAnim * 0.48f), p.openAnim); + Renderer2D.COLOR.svg("x", p.closeX + (p.closeW - icon) * 0.5f, p.closeY + (p.closeH - icon) * 0.5f, icon, icon, SvgRenderOptions.overrideColor(iconColor)); + if (p.closeHoverAnim > 0.08f) SystemCursor.set(SystemCursor.CursorType.HAND); + } + + static void updateHudModePillBounds(SettingsPanelComponent p, float scale) { + p.pillX = p.panelX + 7f * scale; + p.pillY = p.panelY + SettingsPanelComponent.DROPDOWN_HEADER_H * scale + 4f * scale; + p.pillW = p.panelW - 14f * scale; + p.pillH = 16f * scale; + } + + static void renderHudModePill(SettingsPanelComponent p, float scale, int text, int muted, SettingsGuiPalette palette) { + updateHudModePillBounds(p, scale); + + float pillActiveAnim = p.hudPreview.activeAnim(); + float mx = ClickGuiRenderer.getMouseX(); + float my = ClickGuiRenderer.getMouseY(); + float segmentW = p.pillW * 0.5f; + boolean enabledHover = inside(mx, my, p.pillX, p.pillY, segmentW, p.pillH); + boolean onlyHover = inside(mx, my, p.pillX + segmentW, p.pillY, segmentW, p.pillH); + int baseA = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelPillBase(), palette.panelBgLeft(), 0.32f), p.openAnim); + int baseB = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelPillBase(), palette.panelBgRight(), 0.40f), p.openAnim); + int strokeA = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelStroke(), palette.moduleDividerStart(), 0.32f), p.openAnim); + int strokeB = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.moduleDividerEnd(), palette.menuCategorySelectedRight(), 0.20f), p.openAnim); + + ClickGuiRenderer.drawBlur(p.pillX, p.pillY, p.pillW, p.pillH, 5.5f * scale, palette.panelBlurTint(), (105f / 255f) * p.openAnim); + LayoutRender2D.roundedQuad(p.pillX, p.pillY, p.pillW, p.pillH, 5.5f * scale, baseA, baseB, SettingsGuiPalette.darken(baseB, 0.05f), baseA); + LayoutRender2D.roundedStrokeQuad(p.pillX, p.pillY, p.pillW, p.pillH, 5.5f * scale, 0.45f * scale, strokeA, strokeB, LayoutRender2D.alpha(strokeB, 0.82f), LayoutRender2D.alpha(strokeA, 0.88f)); + + float innerPad = 1.3f * scale; + float activeW = segmentW - innerPad * 2f; + float activeH = p.pillH - innerPad * 2f; + float pillT = AnimationUtility.easeInOutCubic(pillActiveAnim); + float activeX = p.pillX + innerPad + (segmentW * pillT); + int activeA = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelPillActive(), palette.menuCategorySelectedLeft(), 0.18f), p.openAnim); + int activeB = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelPillActive(), palette.menuCategorySelectedRight(), 0.24f), p.openAnim); + int activeC = LayoutRender2D.alpha(SettingsGuiPalette.darken(activeB, 0.10f), p.openAnim); + LayoutRender2D.roundedSoftShadow(activeX, p.pillY + innerPad, activeW, activeH, 4.4f * scale, 4.0f * scale, 0.018f, LayoutRender2D.alpha(palette.menuShadow(), p.openAnim * 0.55f)); + LayoutRender2D.roundedQuad(activeX, p.pillY + innerPad, activeW, activeH, 4.4f * scale, activeA, activeB, activeC, activeA); + LayoutRender2D.roundedStrokeQuad(activeX, p.pillY + innerPad, activeW, activeH, 4.4f * scale, 0.28f * scale, LayoutRender2D.alpha(palette.moduleDividerStart(), p.openAnim * 0.86f), LayoutRender2D.alpha(palette.moduleDividerEnd(), p.openAnim * 0.92f), LayoutRender2D.alpha(palette.moduleDividerEnd(), p.openAnim * 0.72f), LayoutRender2D.alpha(palette.moduleDividerStart(), p.openAnim * 0.78f)); + + float sepX = p.pillX + segmentW; + LayoutRender2D.roundedQuad(sepX - 0.42f * scale, p.pillY + 4.0f * scale, 0.84f * scale, p.pillH - 8.0f * scale, 0.42f * scale, + LayoutRender2D.alpha(palette.moduleDividerStart(), 0.35f * p.openAnim), + LayoutRender2D.alpha(palette.moduleDividerEnd(), 0.50f * p.openAnim), + LayoutRender2D.alpha(palette.moduleDividerEnd(), 0.42f * p.openAnim), + LayoutRender2D.alpha(palette.moduleDividerStart(), 0.28f * p.openAnim)); + + float ty = p.pillY + (p.pillH - ClickGuiRenderer.textHeight(ClickGuiRenderer.getInterMedium(), 6.2f * scale)) * 0.5f; + int enabledColor = mixColor(mixColor(text, muted, pillT), text, enabledHover ? 0.10f : 0f); + int onlyColor = mixColor(mixColor(muted, text, pillT), text, onlyHover ? 0.10f : 0f); + ClickGuiRenderer.drawText(ClickGuiRenderer.getInterMedium(), "Enabled", p.pillX + 8.0f * scale, ty, 6.2f * scale, enabledColor, false); + ClickGuiRenderer.drawText(ClickGuiRenderer.getInterMedium(), "Only this", p.pillX + segmentW + 8.0f * scale, ty, 6.2f * scale, onlyColor, false); + } + + static void renderScrollbar(SettingsPanelComponent p, float scale, SettingsGuiPalette palette) { + if (!p.limitedHeight || p.maxScroll <= 0f) return; + + float trackW = 2.5f * scale; + float trackX = p.panelX + p.panelW - 5f * scale; + float trackY = p.contentY + 5f * scale; + float viewableH = Math.max(1f, p.contentH); + float trackH = Math.max(8f * scale, viewableH - 10f * scale); + + float rowsH = Math.max(1f, p.rows.totalHeight()); + float handleH = Math.min(trackH, Math.max(20f * scale, (viewableH / rowsH) * trackH)); + float ratio = p.maxScroll <= 0f ? 0f : (-p.smoothedScroll / p.maxScroll); + float handleY = trackY + (trackH - handleH) * ratio; + + int trTl = LayoutRender2D.alpha(palette.panelScrollTrackA(), p.openAnim); + int trTr = LayoutRender2D.alpha(palette.panelScrollTrackB(), p.openAnim); + int hdTl = LayoutRender2D.alpha(palette.panelScrollHandleA(), p.openAnim); + int hdTr = LayoutRender2D.alpha(palette.panelScrollHandleB(), p.openAnim); + + LayoutRender2D.roundedQuad(trackX, trackY, trackW, trackH, 1.25f * scale, trTl, trTr, trTr, trTl); + LayoutRender2D.roundedQuad(trackX, handleY, trackW, handleH, 1.25f * scale, hdTl, hdTr, hdTr, hdTl); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/panel/SettingsPanelHints.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/panel/SettingsPanelHints.java new file mode 100644 index 0000000..c895c9e --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/panel/SettingsPanelHints.java @@ -0,0 +1,59 @@ +package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.module; + +import net.minecraft.client.Minecraft; +import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiI18n; +import dev.loki.lovisual.features.gui.clickgui.util.tooltip.ClickGuiHintOverlay; +import dev.loki.lovisual.render.engine.math.HudScale; + +/** + * Подсказки внизу экрана для панели настроек: вариант HUD-редактора и + * вариант обычной панели. Общая логика расчёта области — одна. + */ +final class SettingsPanelHints { + + private SettingsPanelHints() {} + + static void renderHudEditor(float fallbackX, float fallbackY, float fallbackW, float fallbackH, + float scale, float alpha) { + ClickGuiHintOverlay.renderBottomLeft( + 0f, 0f, areaWidth(fallbackX, fallbackW), areaHeight(fallbackY, fallbackH), + scale, + alpha, + ClickGuiI18n.tr("clickgui.hints.hud.alt", "Alt - drag without widget linking"), + ClickGuiI18n.tr("clickgui.hints.hud.close", "Esc - close editor"), + ClickGuiI18n.tr("clickgui.hints.hud.hide", "Alt+H - hide hints") + ); + } + + static void renderSettingsPanel(float fallbackX, float fallbackY, float fallbackW, float fallbackH, + float scale, float alpha) { + ClickGuiHintOverlay.renderBottomLeft( + 0f, 0f, areaWidth(fallbackX, fallbackW), areaHeight(fallbackY, fallbackH), + scale, + alpha, + ClickGuiI18n.tr("clickgui.hints.settings_panel.change", "LMB - change focused setting"), + ClickGuiI18n.tr("clickgui.hints.settings_panel.drag", "Drag header - move this panel"), + ClickGuiI18n.tr("clickgui.hints.settings_panel.scroll", "Wheel - scroll settings"), + ClickGuiI18n.tr("clickgui.hints.settings_panel.back", "Esc - back to category"), + ClickGuiI18n.tr("clickgui.hints.settings_panel.hide", "Alt+H - hide hints") + ); + } + + private static float areaWidth(float fallbackX, float fallbackW) { + float areaW = Math.max(1f, fallbackX + fallbackW); + Minecraft mc = Minecraft.getInstance(); + if (mc != null && mc.getWindow() != null) { + areaW = Math.max(1f, HudScale.virtualWidth(mc.getWindow().getWidth(), mc.getWindow().getHeight())); + } + return areaW; + } + + private static float areaHeight(float fallbackY, float fallbackH) { + float areaH = Math.max(1f, fallbackY + fallbackH); + Minecraft mc = Minecraft.getInstance(); + if (mc != null && mc.getWindow() != null) { + areaH = Math.max(1f, HudScale.virtualHeight(mc.getWindow().getWidth(), mc.getWindow().getHeight())); + } + return areaH; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/panel/SettingsPanelHudPreview.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/panel/SettingsPanelHudPreview.java new file mode 100644 index 0000000..4e2e302 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/panel/SettingsPanelHudPreview.java @@ -0,0 +1,78 @@ +package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.module; + +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.module.panel.SettingsPanelHudPreviewImpl; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; + +/** + * Режим превью HUD в панели настроек: ALL_ENABLED / ONLY_CURRENT, + * анимация активного сегмента пилюли и применение режима к реестру + * перетаскиваемых элементов. + */ +final class SettingsPanelHudPreview { + + enum Mode { + ALL_ENABLED, + ONLY_CURRENT + } + + private Mode mode = Mode.ONLY_CURRENT; + private float activeAnim = 1f; + + Mode mode() { + return mode; + } + + boolean isOnlyCurrent() { + return mode == Mode.ONLY_CURRENT; + } + + float activeAnim() { + return activeAnim; + } + + void tick(float dt) { + float pillTarget = mode == Mode.ALL_ENABLED ? 0f : 1f; + activeAnim = AnimationUtility.approach(activeAnim, pillTarget, dt, 14f); + activeAnim = AnimationUtility.snap(activeAnim, pillTarget, 0.002f); + } + + void setModeByPointer(String targetId, float pillX, float pillW, float mx) { + float half = pillW * 0.5f; + mode = (mx < pillX + half) ? Mode.ALL_ENABLED : Mode.ONLY_CURRENT; + apply(targetId); + } + + void resetToOnlyCurrent(String targetId) { + mode = Mode.ONLY_CURRENT; + activeAnim = 1f; + apply(targetId); + } + + void apply(String targetId) { + DraggableHudElement target = DraggableHudElementRegistry.getById(targetId); + if (mode == Mode.ONLY_CURRENT) { + if (target != null) { + target.setPreviewEnabled(true); + } + DraggableHudElementRegistry.setEditorWidgetId(targetId); + DraggableHudElementRegistry.setForceVisible(true); + } else { + if (target != null) { + target.setPreviewEnabled(false); + } + DraggableHudElementRegistry.setEditorWidgetId(null); + DraggableHudElementRegistry.setForceVisible(true); + } + } + + static void reset(String targetId) { + DraggableHudElement target = DraggableHudElementRegistry.getById(targetId); + if (target != null) { + target.setPreviewEnabled(false); + } + DraggableHudElementRegistry.setEditorWidgetId(null); + DraggableHudElementRegistry.setForceVisible(false); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/other/BackgroundComponent.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/other/BackgroundComponent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/other/BackgroundComponent.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/other/BackgroundComponent.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/other/CategoryContainerComponent.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/other/CategoryContainerComponent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/other/CategoryContainerComponent.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/other/CategoryContainerComponent.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/other/SearchComponent.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/other/SearchComponent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/other/SearchComponent.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/other/SearchComponent.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/other/StatusRender.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/other/StatusRender.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/other/StatusRender.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/other/StatusRender.java diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/OnlineRelationPlayerPickerComponent.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/OnlineRelationPlayerPickerComponent.java new file mode 100644 index 0000000..322eeb2 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/OnlineRelationPlayerPickerComponent.java @@ -0,0 +1,74 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * Licensed under the GNU General Public License v3.0. + */ +package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.relations; + +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; +import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiMath; +import dev.loki.lovisual.features.module.modules.misc.DefineTarget; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +/** + * Online player picker for Relations/DefineTarget. + * Facade that delegates data/scroll/render/input to helpers in {@code picker/}. + */ +public final class OnlineRelationPlayerPickerComponent { + final Consumer statusSink; + final OnlinePickerData data = new OnlinePickerData(); + final OnlinePickerScroll scroll = new OnlinePickerScroll(); + private final OnlinePickerRenderer renderer = new OnlinePickerRenderer(); + private final OnlinePickerInput input = new OnlinePickerInput(); + + boolean openTarget; + boolean searchOpen; + boolean searchFocused; + DefineTarget.RelationTargetMode mode = DefineTarget.RelationTargetMode.FRIEND; + + Rect bounds = Rect.ZERO; + Rect searchButton = Rect.ZERO; + Rect searchField = Rect.ZERO; + Rect closeButton = Rect.ZERO; + Rect listRect = Rect.ZERO; + + float openAnim; + float searchAnim; + final List rowHits = new ArrayList<>(); + + OnlineRelationPlayerPickerComponent(Consumer statusSink) { this.statusSink = statusSink; } + + boolean isVisible() { return openTarget || openAnim > 0.002f; } + boolean blocksMovementInput() { return openTarget && searchFocused; } + boolean isOpen() { return openTarget; } + boolean isSearchFocused() { return openTarget && searchFocused; } + String searchText() { return data.searchText(); } + void focusSearch() { input.focusSearch(this); } + void clearSearch() { input.clearSearch(this); } + void open(DefineTarget.RelationTargetMode initialMode) { + openTarget = true; searchOpen = false; + if (initialMode != null) mode = initialMode; + resetScroll(); data.refreshNow(); + } + void close() { + openTarget = false; searchFocused = false; scroll.draggingScrollbar = false; + if (!data.search.isEmpty()) { data.search = ""; data.invalidateFilter(); } + } + void toggle(DefineTarget.RelationTargetMode initialMode) { if (openTarget) close(); else open(initialMode); } + void resetScroll() { scroll.reset(); } + void render(float x, float y, float w, float h, float mx, float my, float scale, SettingsGuiPalette palette) { renderer.render(this, x, y, w, h, mx, my, scale, palette); } + boolean click(float mx, float my, int button) { return input.click(this, mx, my, button); } + boolean mousePressedScrollbar(float mx, float my, int button) { return input.mousePressedScrollbar(this, mx, my, button); } + void mouseReleased(int button) { input.mouseReleased(this, button); } + void scroll(float mx, float my, double amount) { input.scroll(this, mx, my, amount); } + boolean keyPressed(int keyCode, int scanCode, int modifiers) { return input.keyPressed(this, keyCode, scanCode, modifiers); } + boolean charTyped(char chr, int modifiers) { return input.charTyped(this, chr, modifiers); } + + record RowHit(String name, Rect rect) {} + record Rect(float x, float y, float w, float h) { + static final Rect ZERO = new Rect(0f, 0f, 0f, 0f); + boolean contains(float mx, float my) { return ClickGuiMath.insideRect(mx, my, x, y, w, h); } + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/RelationPlayerCardComponent.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/RelationPlayerCardComponent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/RelationPlayerCardComponent.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/RelationPlayerCardComponent.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/RelationsComponent.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/RelationsComponent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/RelationsComponent.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/RelationsComponent.java diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/picker/OnlinePickerData.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/picker/OnlinePickerData.java new file mode 100644 index 0000000..b202d44 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/picker/OnlinePickerData.java @@ -0,0 +1,172 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * Licensed under the GNU General Public License v3.0. + */ +package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.relations; + +import com.mojang.authlib.GameProfile; +import dev.loki.lovisual.features.relations.PlayerRelations; +import dev.loki.lovisual.features.relations.category.CategoryService; +import dev.loki.lovisual.features.relations.category.CategoryType; +import dev.loki.lovisual.util.player.health.PlayerSkinResolver; +import dev.loki.lovisual.util.text.format.LegacyTextUtil; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.PlayerInfo; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.world.level.GameType; +import java.util.*; + +final class OnlinePickerData { + private static final long STRUCTURE_REFRESH_MS = 350L; + private static final long DYNAMIC_REFRESH_MS = 250L; + final List rows = new ArrayList<>(); + final Map skinCache = new HashMap<>(); + String search = ""; + int rowsVersion; + int filteredRowsVersion = -1; + String filteredSearchKey = ""; + List filteredRowsCache = List.of(); + long lastStructureCheckMs; + long lastDynamicRefreshMs; + long lastStructureSignature; + int lastOnlineCount = -1; + String searchText() { return search == null ? "" : search; } + String normalizedSearch() { return search == null ? "" : LegacyTextUtil.stripLegacy(search).trim().toLowerCase(Locale.ROOT); } + void invalidateFilter() { filteredRowsVersion = -1; } + List filteredRows() { + String q = normalizedSearch(); + if (filteredRowsVersion == rowsVersion && filteredSearchKey.equals(q)) return filteredRowsCache; + if (q.isBlank()) filteredRowsCache = rows; + else { + List out = new ArrayList<>(); + for (PlayerRow r : rows) if (r.searchText().contains(q)) out.add(r); + filteredRowsCache = out; + } + filteredRowsVersion = rowsVersion; + filteredSearchKey = q; + return filteredRowsCache; + } + Identifier resolveSkin(PlayerRow p) { + if (p == null || p.id() == null) return null; + Identifier c = skinCache.get(p.id()); + if (c != null) return c; + Identifier s = PlayerSkinResolver.resolveProfileSkin(p.profile()); + if (s != null) skinCache.put(p.id(), s); + return s; + } + void refreshIfNeeded() { + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.getConnection() == null) return; + long now = System.currentTimeMillis(); + int online = onlineCount(); + boolean check = rows.isEmpty() || online != lastOnlineCount || now - lastStructureCheckMs >= STRUCTURE_REFRESH_MS; + if (check) { + long sig = structureSignature(); + lastStructureCheckMs = now; + if (rows.isEmpty() || online != lastOnlineCount || sig != lastStructureSignature) { + collectRows(); + lastOnlineCount = online; + lastStructureSignature = sig; + lastDynamicRefreshMs = now; + return; + } + } + if (now - lastDynamicRefreshMs >= DYNAMIC_REFRESH_MS) { + refreshDynamicRows(); + lastDynamicRefreshMs = now; + } + } + void refreshNow() { + collectRows(); + lastOnlineCount = onlineCount(); + lastStructureSignature = structureSignature(); + lastStructureCheckMs = System.currentTimeMillis(); + lastDynamicRefreshMs = lastStructureCheckMs; + } + int onlineCount() { + Minecraft mc = Minecraft.getInstance(); + return mc != null && mc.getConnection() != null ? mc.getConnection().getListedOnlinePlayers().size() : 0; + } + void collectRows() { + Minecraft mc = Minecraft.getInstance(); + rows.clear(); + if (mc == null || mc.getConnection() == null) { rowsVersion++; invalidateFilter(); return; } + Collection online = mc.getConnection().getListedOnlinePlayers(); + List sorted = new ArrayList<>(online); + sorted.sort(Comparator.comparingInt((PlayerInfo i) -> -i.getTabListOrder()).thenComparing(i -> i.getGameMode() == GameType.SPECTATOR).thenComparing(i -> teamName(i).toLowerCase(Locale.ROOT)).thenComparing(i -> profileName(i).toLowerCase(Locale.ROOT), String.CASE_INSENSITIVE_ORDER)); + Set live = new HashSet<>(); + for (PlayerInfo info : sorted) { + if (info == null || info.getProfile() == null) continue; + GameProfile p = info.getProfile(); + UUID id = p.id(); + String name = p.name(); + if (id == null || name == null || name.isBlank()) continue; + Component d = displayName(info, name); + CategoryType rel = CategoryService.get(name); + int col = rel == CategoryType.DEFAULT ? PlayerRelations.get().colorDefault() : CategoryService.getColor(name); + rows.add(new PlayerRow(id, p, name, d, searchText(name, d), rel, col, info.getLatency(), info.getGameMode() == GameType.SPECTATOR)); + live.add(id); + } + skinCache.keySet().retainAll(live); + rowsVersion++; + invalidateFilter(); + } + void refreshDynamicRows() { + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.getConnection() == null || rows.isEmpty()) return; + boolean changed = false; + for (int i = 0; i < rows.size(); i++) { + PlayerRow r = rows.get(i); + PlayerInfo info = mc.getConnection().getPlayerInfo(r.id()); + if (info == null || info.getProfile() == null) { collectRows(); return; } + CategoryType rel = CategoryService.get(r.name()); + int col = rel == CategoryType.DEFAULT ? PlayerRelations.get().colorDefault() : CategoryService.getColor(r.name()); + int lat = info.getLatency(); + boolean spec = info.getGameMode() == GameType.SPECTATOR; + if (lat == r.latency() && spec == r.spectator() && rel == r.relation() && col == r.relationColor()) continue; + rows.set(i, new PlayerRow(r.id(), r.profile(), r.name(), r.displayName(), r.searchText(), rel, col, lat, spec)); + changed = true; + } + if (changed) { rowsVersion++; invalidateFilter(); } + } + long structureSignature() { + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.getConnection() == null) return 0L; + long h = 0x6A09E667F3BCC909L; + for (PlayerInfo info : mc.getConnection().getListedOnlinePlayers()) { + if (info == null || info.getProfile() == null) continue; + GameProfile p = info.getProfile(); + h = mix(h, p.id()); h = mix(h, p.name()); h = mix(h, info.getTabListOrder()); + h = mix(h, info.getGameMode() == GameType.SPECTATOR ? 1 : 0); h = mix(h, teamName(info)); + Component raw = info.getTabListDisplayName(); h = mix(h, raw != null ? raw.getString() : ""); + String name = p.name() == null ? "" : p.name(); + CategoryType rel = CategoryService.get(name); + h = mix(h, rel.ordinal()); h = mix(h, rel == CategoryType.DEFAULT ? PlayerRelations.get().colorDefault() : CategoryService.getColor(name)); + } + return h; + } + Component displayName(PlayerInfo info, String fallback) { + Minecraft mc = Minecraft.getInstance(); + Component d = null; + if (mc != null && mc.gui != null && mc.gui.hud != null && mc.gui.hud.getTabList() != null) d = mc.gui.hud.getTabList().getNameForDisplay(info); + if (d == null) d = info.getTabListDisplayName(); + if (d == null || d.getString().isBlank()) d = Component.literal(fallback); + return LegacyTextUtil.convertLegacyCodes(d); + } + static String searchText(String name, Component d) { + String raw = (name == null ? "" : name) + " " + (d == null ? "" : d.getString()); + return LegacyTextUtil.stripLegacy(raw).trim().toLowerCase(Locale.ROOT); + } + static long mix(long h, int v) { return (h ^ v) * 0x100000001B3L; } + static long mix(long h, UUID v) { + if (v == null) return mix(h, 0); + h = (h ^ v.getMostSignificantBits()) * 0x100000001B3L; + return (h ^ v.getLeastSignificantBits()) * 0x100000001B3L; + } + static long mix(long h, String v) { return (h ^ (v != null ? v.hashCode() : 0)) * 0x100000001B3L; } + static String teamName(PlayerInfo i) { return i != null && i.getTeam() != null && i.getTeam().getName() != null ? i.getTeam().getName() : ""; } + static String profileName(PlayerInfo i) { GameProfile p = i != null ? i.getProfile() : null; return p != null && p.name() != null ? p.name() : ""; } + record PlayerRow(UUID id, GameProfile profile, String name, Component displayName, String searchText, CategoryType relation, int relationColor, int latency, boolean spectator) {} +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/picker/OnlinePickerInput.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/picker/OnlinePickerInput.java new file mode 100644 index 0000000..987b04b --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/picker/OnlinePickerInput.java @@ -0,0 +1,87 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * Licensed under the GNU General Public License v3.0. + */ +package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.relations; + +import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiI18n; +import dev.loki.lovisual.features.module.modules.misc.DefineTarget; +import dev.loki.lovisual.util.text.mask.ChatNameUtil; +import org.lwjgl.glfw.GLFW; + +final class OnlinePickerInput { + private static final String I18N = "clickgui.settings.relations.online_picker."; + boolean click(OnlineRelationPlayerPickerComponent host, float mx, float my, int button) { + if (!host.openTarget || button != 0) return false; + if (host.closeButton.contains(mx, my)) { host.close(); return true; } + if (host.searchButton.contains(mx, my)) { focusSearch(host); return true; } + if (host.searchField.contains(mx, my)) { focusSearch(host); return true; } + for (OnlineRelationPlayerPickerComponent.RowHit hit : host.rowHits) { + if (!hit.rect().contains(mx, my)) continue; + addPlayer(host, hit.name()); + return true; + } + if (host.bounds.contains(mx, my)) { host.searchFocused = false; return true; } + return false; + } + boolean mousePressedScrollbar(OnlineRelationPlayerPickerComponent host, float mx, float my, int button) { + if (!host.openTarget || button != 0) return false; + return host.scroll.press(mx, my, button); + } + void mouseReleased(OnlineRelationPlayerPickerComponent host, int button) { host.scroll.release(button); } + void scroll(OnlineRelationPlayerPickerComponent host, float mx, float my, double amount) { + if (!host.openTarget || !host.listRect.contains(mx, my)) return; + host.scroll.addScroll((float) (amount * 24f)); + } + boolean keyPressed(OnlineRelationPlayerPickerComponent host, int keyCode, int scanCode, int modifiers) { + if (!host.openTarget) return false; + boolean ctrl = (modifiers & GLFW.GLFW_MOD_CONTROL) != 0; + if (ctrl && keyCode == GLFW.GLFW_KEY_F) { focusSearch(host); return true; } + if (keyCode == GLFW.GLFW_KEY_ESCAPE) { + if (host.searchFocused) { host.searchFocused = false; return true; } + host.close(); return true; + } + if (!host.searchFocused) return false; + if (keyCode == GLFW.GLFW_KEY_ENTER || keyCode == GLFW.GLFW_KEY_KP_ENTER) { host.searchFocused = false; return true; } + if (keyCode == GLFW.GLFW_KEY_BACKSPACE) { + if (!host.data.search.isEmpty()) { host.data.search = host.data.search.substring(0, host.data.search.length() - 1); host.scroll.reset(); host.data.invalidateFilter(); } + return true; + } + return true; + } + boolean charTyped(OnlineRelationPlayerPickerComponent host, char chr, int modifiers) { + if (!host.openTarget || !host.searchFocused) return false; + if (chr >= 32 && chr != 127) { + host.data.search += chr; + if (host.data.search.length() > 32) host.data.search = host.data.search.substring(0, 32); + host.scroll.reset(); + host.data.invalidateFilter(); + } + return true; + } + void focusSearch(OnlineRelationPlayerPickerComponent host) { + if (!host.openTarget) return; + host.searchFocused = true; + host.searchOpen = false; + } + void clearSearch(OnlineRelationPlayerPickerComponent host) { + if (host.data.search == null || host.data.search.isEmpty()) return; + host.data.search = ""; + host.scroll.reset(); + host.data.invalidateFilter(); + } + void addPlayer(OnlineRelationPlayerPickerComponent host, String rawName) { + String name = ChatNameUtil.normalizeNickCandidate(rawName); + if (!ChatNameUtil.isNickLike(name)) { setStatus(host, tr("invalid", "Invalid nick")); return; } + String modeName = modeDisplayName(host.mode); + boolean wasIn = DefineTarget.isPlayerInRelationMode(host.mode, name); + boolean added = DefineTarget.togglePlayerRelation(host.mode, name); + host.data.refreshNow(); + if (wasIn && !added) setStatus(host, tr("removed", "Removed %s from %s", name, modeName)); + else setStatus(host, tr("added", "Added %s as %s", name, modeName)); + } + private void setStatus(OnlineRelationPlayerPickerComponent host, String text) { if (host.statusSink != null) host.statusSink.accept(text); } + private static String modeDisplayName(DefineTarget.RelationTargetMode m) { if (m == null) return ""; return tr("mode_name." + m.id(), m.fallbackLabel()); } + private static String tr(String k, String f, Object... a) { return ClickGuiI18n.tr(I18N + k, f, a); } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/picker/OnlinePickerRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/picker/OnlinePickerRenderer.java new file mode 100644 index 0000000..09224b9 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/picker/OnlinePickerRenderer.java @@ -0,0 +1,192 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * Licensed under the GNU General Public License v3.0. + */ +package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.relations; + +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.LayoutRender2D; +import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; +import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiI18n; +import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiMath; +import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiRichTextRenderer; +import dev.loki.lovisual.features.module.modules.misc.DefineTarget; +import dev.loki.lovisual.features.relations.PlayerRelations; +import dev.loki.lovisual.features.relations.category.CategoryType; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.color.RenderColor; +import dev.loki.lovisual.render.engine.core.context.ViewportContext; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.svg.model.SvgRenderOptions; +import dev.loki.lovisual.render.helpers.clip.ScissorFunction; +import dev.loki.lovisual.render.helpers.player.PlayerHeadRenderer; +import dev.loki.lovisual.render.helpers.util.SystemCursor; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.resources.Identifier; +import java.util.List; + +final class OnlinePickerRenderer { + private static final String I18N = "clickgui.settings.relations.online_picker."; + void render(OnlineRelationPlayerPickerComponent host, float x, float y, float w, float h, float mx, float my, float scale, SettingsGuiPalette palette) { + float dt = AnimationUtility.deltaTime(); + host.openAnim = AnimationUtility.approach(host.openAnim, host.openTarget ? 1f : 0f, dt, host.openTarget ? 13f : 18f); + host.openAnim = AnimationUtility.snap(host.openAnim, host.openTarget ? 1f : 0f, 0.003f); + host.searchAnim = AnimationUtility.approach(host.searchAnim, host.searchFocused ? 1f : 0f, dt, 12f); + host.searchAnim = AnimationUtility.snap(host.searchAnim, host.searchFocused ? 1f : 0f, 0.004f); + if (!host.isVisible()) return; + host.bounds = new OnlineRelationPlayerPickerComponent.Rect(x, y, w, h); + host.rowHits.clear(); + host.data.refreshIfNeeded(); + float enter = AnimationUtility.easeOutBack(host.openAnim, 0.42f); + float exit = AnimationUtility.easeInOutCubic(host.openAnim); + float visual = host.openTarget ? enter : exit; + float opacity = host.openTarget ? AnimationUtility.clamp01(host.openAnim * 1.16f) : AnimationUtility.smoothstep(host.openAnim); + float radius = 7.5f * scale; + float lift = host.openTarget ? (1f - visual) * 4f * scale : 0f; + float panelY = y + lift; + float panelH = host.openTarget ? h : Math.max(1f, h * (0.74f + 0.26f * visual)); + float panelBottom = panelY + panelH; + ClickGuiRenderer.drawBlur(x, panelY, w, panelH, radius, palette.panelBlurTint(), (170f / 255f) * opacity); + LayoutRender2D.roundedSoftShadow(x, panelY + 2f * scale, w, panelH, radius, 15f * scale, 0.18f * opacity, fade(SettingsGuiPalette.withAlpha(0xFF000000, 140), opacity)); + LayoutRender2D.roundedQuad(x, panelY, w, panelH, radius, fade(SettingsGuiPalette.withAlpha(SettingsGuiPalette.mix(palette.panelBgLeft(), palette.menuCategorySelectedLeft(), 0.18f), 216), opacity), fade(SettingsGuiPalette.withAlpha(SettingsGuiPalette.mix(palette.panelBgRight(), palette.menuCategorySelectedRight(), 0.16f), 208), opacity), fade(SettingsGuiPalette.withAlpha(SettingsGuiPalette.darken(palette.panelBgRight(), 0.12f), 214), opacity), fade(SettingsGuiPalette.withAlpha(SettingsGuiPalette.darken(palette.panelBgLeft(), 0.10f), 220), opacity)); + Renderer2D.COLOR.radialGlowMasked(x, panelY, w, panelH, radius, 0f, 92f * scale, x + 42f * scale, panelY + 30f * scale, LayoutRender2D.alpha(modeColor(host), 0.20f * opacity)); + LayoutRender2D.roundedStrokeQuad(x, panelY, w, panelH, radius, 0.65f * scale, LayoutRender2D.alpha(modeColor(host), 0.42f * opacity), LayoutRender2D.alpha(palette.menuWindowStroke(), 0.78f * opacity), LayoutRender2D.alpha(palette.menuWindowStroke(), 0.50f * opacity), LayoutRender2D.alpha(modeColor(host), 0.26f * opacity)); + float pad = 8f * scale; + float headerH = 28f * scale; + renderHeader(host, x + pad, panelY + pad, w - pad * 2f, headerH - pad, mx, my, scale, palette, opacity); + float listX = x + pad; + float listY = panelY + headerH + 4f * scale; + float listW = w - pad * 2f; + float listH = Math.max(1f, panelBottom - pad - listY); + host.listRect = new OnlineRelationPlayerPickerComponent.Rect(listX, listY, listW, listH); + renderRows(host, listX, listY, listW, listH, mx, my, scale, palette, opacity); + } + private void renderHeader(OnlineRelationPlayerPickerComponent host, float x, float y, float w, float h, float mx, float my, float scale, SettingsGuiPalette palette, float opacity) { + ClickGuiRenderer.drawText(ClickGuiRenderer.getInterMedium(), tr("title", "Online players"), x, y + 1.5f * scale, 8.2f * scale, fade(palette.panelText(), opacity), false); + String subtitle = host.data.search == null || host.data.search.isBlank() ? tr("subtitle", "Pick from current tab list") : tr("subtitle_search", "%s / %s visible", host.data.filteredRows().size(), host.data.rows.size()); + ClickGuiRenderer.drawText(ClickGuiRenderer.getInterRegular(), subtitle, x, y + 13f * scale, 6.15f * scale, fade(palette.panelMuted(), opacity), false); + float btn = 17f * scale; + host.closeButton = new OnlineRelationPlayerPickerComponent.Rect(x + w - btn, y, btn, btn); + host.searchButton = OnlineRelationPlayerPickerComponent.Rect.ZERO; + host.searchField = OnlineRelationPlayerPickerComponent.Rect.ZERO; + drawSmallIconButton(host.closeButton, "x", false, host.closeButton.contains(mx, my), scale, palette, opacity); + } + private void renderRows(OnlineRelationPlayerPickerComponent host, float x, float y, float w, float h, float mx, float my, float scale, SettingsGuiPalette palette, float opacity) { + List filtered = host.data.filteredRows(); + if (filtered.isEmpty()) { + String t = host.data.search.isBlank() ? tr("empty", "No online players.") : tr("empty_search", "No matching online players."); + ClickGuiRenderer.drawText(ClickGuiRenderer.getInterRegular(), t, x + 5f * scale, y + 5f * scale, 7.2f * scale, fade(palette.panelMuted(), opacity), false); + host.scroll.updateMetrics(x, y, w, h, 0f, scale); + return; + } + float reserved = 9f * scale; + float gap = 5f * scale; + float rowH = 31f * scale; + float stepY = rowH + gap; + float contentH = filtered.size() * stepY - gap; + float maxScroll = Math.max(0f, contentH - h); + host.scroll.updateMetrics(x, y, w, h, maxScroll, scale); + float rowAreaW = host.scroll.scrollbarVisible ? Math.max(1f, w - reserved) : w; + host.scroll.dragIfNeeded(); + host.scroll.clampAndSmooth(maxScroll); + boolean clipped = ScissorFunction.pushRaw(x, y, w, h); + for (int i = 0; i < filtered.size(); i++) { + float rowY = y + i * stepY + host.scroll.smoothedScroll; + if (rowY + rowH < y || rowY > y + h) continue; + renderRow(host, filtered.get(i), x, rowY, rowAreaW, rowH, mx, my, scale, palette, i, opacity); + } + if (clipped) ScissorFunction.pop(); + host.scroll.renderScrollbar(x, y, w, h, maxScroll, scale, palette, opacity); + } + private void renderRow(OnlineRelationPlayerPickerComponent host, OnlinePickerData.PlayerRow player, float x, float y, float w, float h, float mx, float my, float scale, SettingsGuiPalette palette, int index, float opacity) { + float base = host.openTarget ? AnimationUtility.easeOutCubic(host.openAnim) : 1f; + float anim = AnimationUtility.clamp((base - index * 0.018f) / 0.72f, 0f, 1f); + y += (1f - (host.openTarget ? AnimationUtility.easeOutCubic(anim) : 1f)) * 7f * scale; + boolean hover = host.openTarget && ClickGuiMath.insideRect(mx, my, x, y, w, h); + boolean selected = DefineTarget.isPlayerInRelationMode(host.mode, player.name()); + int relCol = player.relationColor() != 0 ? player.relationColor() : modeColor(host); + int top = hover ? SettingsGuiPalette.mix(palette.moduleCardTop(), modeColor(host), 0.10f) : palette.moduleCardTop(); + int topStrong = hover || selected ? SettingsGuiPalette.mix(palette.moduleCardTopStrong(), relCol, selected ? 0.18f : 0.10f) : palette.moduleCardTopStrong(); + int bottom = hover ? SettingsGuiPalette.mix(palette.moduleCardBottom(), modeColor(host), 0.12f) : palette.moduleCardBottom(); + int bottomStrong = selected ? SettingsGuiPalette.mix(palette.moduleCardBottomStrong(), relCol, 0.20f) : palette.moduleCardBottomStrong(); + LayoutRender2D.roundedSoftShadow(x, y + 1f * scale, w, h, 5.5f * scale, 8f * scale, 0.08f, fade(SettingsGuiPalette.withAlpha(0xFF000000, hover ? 96 : 58), opacity)); + LayoutRender2D.roundedQuad(x, y, w, h, 5.5f * scale, fade(top, opacity), fade(topStrong, opacity), fade(bottomStrong, opacity), fade(bottom, opacity)); + Renderer2D.COLOR.radialGlowMasked(x, y, w, h, 5.5f * scale, 0f, 42f * scale, x + 15f * scale, y + h * 0.50f, LayoutRender2D.alpha(relCol, (selected ? 0.28f : 0.14f) * opacity)); + LayoutRender2D.roundedStrokeQuad(x, y, w, h, 5.5f * scale, 0.55f * scale, LayoutRender2D.alpha(relCol, (selected ? 0.70f : 0.34f) * opacity), LayoutRender2D.alpha(palette.menuWindowStroke(), 0.72f * opacity), LayoutRender2D.alpha(palette.menuWindowStroke(), 0.44f * opacity), LayoutRender2D.alpha(relCol, (selected ? 0.46f : 0.20f) * opacity)); + float head = 18.5f * scale; + float headX = x + 6f * scale; + float headY = y + (h - head) * 0.5f; + renderHead(host.data.resolveSkin(player), headX, headY, head, scale, palette, opacity); + float dot = 5.3f * scale; + float dotX = headX + head - dot * 0.72f; + float dotY = headY + head - dot * 0.72f; + Renderer2D.COLOR.radialGlowMasked(dotX - 4f * scale, dotY - 4f * scale, dot + 8f * scale, dot + 8f * scale, (dot + 8f * scale) * 0.5f, 0f, 11f * scale, dotX + dot * 0.5f, dotY + dot * 0.5f, LayoutRender2D.alpha(relCol, 0.48f * opacity)); + Renderer2D.COLOR.roundedRect(dotX, dotY, dot, dot, dot * 0.5f, 0.7f, fade(relCol | 0xFF000000, opacity)); + Renderer2D.COLOR.roundedRectStroke(dotX, dotY, dot, dot, dot * 0.5f, 0.7f, 0.55f * scale, LayoutRender2D.alpha(0xFFFFFFFF, 0.42f * opacity)); + float textX = headX + head + 7f * scale; + float rightPad = selected ? 56f * scale : 44f * scale; + float pingW = Math.max(38f * scale, ClickGuiRenderer.textWidth(ClickGuiRenderer.getInterRegular(), pingText(player.latency()), 6.2f * scale) + 26f * scale); + float nameW = Math.max(20f * scale, w - (textX - x) - rightPad - pingW); + int nameColor = player.spectator() ? palette.panelMuted() : palette.panelText(); + ClickGuiRichTextRenderer.draw(player.displayName(), textX, y + 6f * scale, nameW, 8f * scale, nameColor, opacity, false); + String relation = relationLabel(player.relation()); + ClickGuiRenderer.drawText(ClickGuiRenderer.getInterRegular(), ClickGuiRenderer.fitText(ClickGuiRenderer.getInterRegular(), relation, 5.5f * scale, nameW), textX, y + 18.6f * scale, 5.5f * scale, fade(SettingsGuiPalette.mix(palette.panelMuted(), relCol | 0xFF000000, 0.45f), opacity), false); + float pingH = 13f * scale; + drawPingChip(player.latency(), x + w - 6f * scale - pingW, y + (h - pingH) * 0.5f, pingW, pingH, scale, palette, opacity); + OnlineRelationPlayerPickerComponent.Rect hit = new OnlineRelationPlayerPickerComponent.Rect(x, y, w, h); + host.rowHits.add(new OnlineRelationPlayerPickerComponent.RowHit(player.name(), hit)); + if (hover) SystemCursor.set(SystemCursor.CursorType.HAND); + } + private void renderHead(Identifier skin, float x, float y, float size, float scale, SettingsGuiPalette palette, float opacity) { + float o = AnimationUtility.clamp01(opacity * ClickGuiRenderer.getRenderAlphaMultiplier()); + if (o <= 0.01f) return; + GuiGraphicsExtractor ctx = ViewportContext.getCurrentContext(); + if (skin != null && ctx != null) { + int a = Math.round(255f * o); + PlayerHeadRenderer.drawRounded(ctx, x, y, size, 4.2f * scale, skin, new RenderColor(255, 255, 255, a), true, new RenderColor(255, 255, 255, Math.round(48f * o)), 0.95f * scale, false); + return; + } + Renderer2D.COLOR.roundedRect(x, y, size, size, 4.2f * scale, 1f, LayoutRender2D.alpha(palette.panelMuted(), 0.22f * o)); + Renderer2D.COLOR.roundedRectStroke(x, y, size, size, 4.2f * scale, 1f, 0.55f * scale, LayoutRender2D.alpha(palette.panelMuted(), 0.62f * o)); + } + private void drawPingChip(int latency, float x, float y, float w, float h, float scale, SettingsGuiPalette palette, float opacity) { + String text = pingText(latency); + int base = pingBaseColor(latency); + int chipA = LayoutRender2D.alpha(palette.panelPillBase(), 0.84f * opacity); + int chipB = LayoutRender2D.alpha(SettingsGuiPalette.mix(palette.panelPillBase(), palette.moduleCardBottomStrong(), 0.38f), 0.52f * opacity); + LayoutRender2D.roundedQuad(x, y, w, h, 5.4f * scale, chipA, chipA, chipB, chipB); + LayoutRender2D.roundedStrokeQuad(x, y, w, h, 5.4f * scale, 0.55f * scale, LayoutRender2D.alpha(palette.panelStroke(), 0.40f * opacity), LayoutRender2D.alpha(palette.moduleDividerEnd(), 0.26f * opacity), LayoutRender2D.alpha(palette.moduleDividerEnd(), 0.18f * opacity), LayoutRender2D.alpha(palette.panelStroke(), 0.30f * opacity)); + float cx = x + 7.3f * scale; + float cy = y + h * 0.5f; + float r = 3.1f * scale; + Renderer2D.COLOR.circleSoftShadow(cx, cy, r + 1.5f * scale, 5f * scale, 0.34f, LayoutRender2D.alpha(base, 0.48f * opacity)); + Renderer2D.COLOR.circle(cx, cy, r + 0.95f * scale, 0.8f * scale, LayoutRender2D.alpha(base, 0.30f * opacity)); + Renderer2D.COLOR.circle(cx, cy, r, 0.75f * scale, LayoutRender2D.alpha(base, opacity)); + Renderer2D.COLOR.circleStroke(cx, cy, r + 0.72f * scale, 0.55f * scale, 0.8f * scale, LayoutRender2D.alpha(0xFFFFFFFF, 0.36f * opacity)); + Renderer2D.COLOR.arcStrokeGradient(cx, cy, r + 1.55f * scale, 0.9f * scale, -140f, 70f, 0.45f * scale, LayoutRender2D.alpha(base, 0.68f * opacity), LayoutRender2D.alpha(0xFFFFFFFF, 0.16f * opacity), 35f, 0f); + ClickGuiRenderer.drawText(ClickGuiRenderer.getInterMedium(), text, x + 14.5f * scale, y + 3.3f * scale, 6.2f * scale, fade(palette.panelText(), opacity), false); + } + private void drawSmallIconButton(OnlineRelationPlayerPickerComponent.Rect rect, String icon, boolean active, boolean hover, float scale, SettingsGuiPalette palette, float opacity) { + int color = active ? modeColor(null) : palette.panelMuted(); + // host required for modeColor; if null fallback to panelMuted + int bgA = active || hover ? SettingsGuiPalette.mix(palette.panelPillBase(), color, active ? 0.30f : 0.14f) : LayoutRender2D.alpha(palette.panelPillBase(), 0.82f); + int bgB = active || hover ? SettingsGuiPalette.mix(palette.moduleCardTopStrong(), color, active ? 0.18f : 0.08f) : LayoutRender2D.alpha(palette.panelPillBase(), 0.70f); + LayoutRender2D.roundedQuad(rect.x(), rect.y(), rect.w(), rect.h(), 4.3f * scale, fade(bgA, opacity), fade(bgB, opacity), fade(bgB, opacity), fade(bgA, opacity)); + LayoutRender2D.roundedStroke(rect.x(), rect.y(), rect.w(), rect.h(), 4.3f * scale, 0.45f * scale, LayoutRender2D.alpha(color, (active || hover ? 0.56f : 0.24f) * opacity)); + Renderer2D.COLOR.svg(icon, rect.x() + 4.1f * scale, rect.y() + 4.1f * scale, rect.w() - 8.2f * scale, rect.h() - 8.2f * scale, SvgRenderOptions.overrideColor(fade(active ? palette.panelText() : SettingsGuiPalette.mix(palette.panelMuted(), palette.panelText(), hover ? 0.25f : 0f), opacity))); + if (hover) SystemCursor.set(SystemCursor.CursorType.HAND); + } + static int fade(int c, float o) { return LayoutRender2D.alpha(c, AnimationUtility.clamp01(o)); } + static int modeColor(OnlineRelationPlayerPickerComponent host) { + if (host == null) return 0xFF8A8A8A; + return host.mode.color(PlayerRelations.get()) | 0xFF000000; + } + static int pingBaseColor(int l) { if (l < 0) return 0xFF8993A3; if (l < 90) return 0xFF52F2A1; if (l < 180) return 0xFFFFD166; return 0xFFFF5A6E; } + static String pingText(int l) { return l < 0 ? "?" : String.valueOf(l); } + static String relationLabel(CategoryType t) { + if (t == null) return tr("relation.default", "Default"); + return switch (t) { case FRIEND -> tr("relation.friend", "Friend"); case ENEMY -> tr("relation.enemy", "Enemy"); case STAFF -> tr("relation.staff", "Staff"); case BEDWARS_SELF -> tr("relation.bedwars_self", "Bedwars Self"); case BEDWARS_ENEMY -> tr("relation.bedwars_enemy", "Bedwars Enemy"); default -> tr("relation.default", "Default"); }; + } + static String tr(String k, String f, Object... a) { return ClickGuiI18n.tr(I18N + k, f, a); } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/picker/OnlinePickerScroll.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/picker/OnlinePickerScroll.java new file mode 100644 index 0000000..6bb15f6 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/picker/OnlinePickerScroll.java @@ -0,0 +1,78 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * Licensed under the GNU General Public License v3.0. + */ +package dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.relations; + +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.LayoutRender2D; +import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; +import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiMath; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.helpers.util.SystemCursor; + +final class OnlinePickerScroll { + float scroll; + float smoothedScroll; + boolean draggingScrollbar; + boolean scrollbarVisible; + float scrollbarX; + float scrollbarY; + float scrollbarW; + float scrollbarH; + float scrollbarThumbY; + float scrollbarThumbH; + float scrollbarMaxScroll; + float scrollbarDragOffset; + + void reset() { scroll = 0f; smoothedScroll = 0f; draggingScrollbar = false; } + void clampAndSmooth(float maxScroll) { + scroll = ClickGuiMath.clamp(scroll, -maxScroll, 0f); + smoothedScroll = AnimationUtility.approach(smoothedScroll, scroll, draggingScrollbar ? 0.55f : 0.22f); + smoothedScroll = AnimationUtility.snap(smoothedScroll, scroll, draggingScrollbar ? 0.01f : 0.05f); + } + void addScroll(float amount) { scroll += amount; } + void updateMetrics(float x, float y, float w, float h, float maxScroll, float scale) { + scrollbarVisible = maxScroll > 0.5f; + scrollbarMaxScroll = Math.max(0f, maxScroll); + if (!scrollbarVisible) { draggingScrollbar = false; scrollbarX = scrollbarY = scrollbarW = scrollbarH = scrollbarThumbY = scrollbarThumbH = 0f; return; } + scrollbarW = 3f * scale; + scrollbarX = x + w - scrollbarW - 2f * scale; + scrollbarY = y + 3f * scale; + scrollbarH = Math.max(1f, h - 6f * scale); + scrollbarThumbH = Math.max(18f * scale, scrollbarH * (scrollbarH / (scrollbarMaxScroll + scrollbarH))); + float ratio = scrollbarMaxScroll <= 0f ? 0f : (-smoothedScroll / scrollbarMaxScroll); + scrollbarThumbY = scrollbarY + (scrollbarH - scrollbarThumbH) * AnimationUtility.clamp(ratio, 0f, 1f); + } + boolean isHovered(float mx, float my) { + if (!scrollbarVisible) return false; + float pad = 4f; + return ClickGuiMath.insideRect(mx, my, scrollbarX - pad, scrollbarY - pad, scrollbarW + pad * 2f, scrollbarH + pad * 2f); + } + void scrollToMouse(float my) { + if (!scrollbarVisible || scrollbarMaxScroll <= 0f) return; + float span = scrollbarH - scrollbarThumbH; + if (span <= 0.5f) return; + float top = AnimationUtility.clamp(my - scrollbarDragOffset, scrollbarY, scrollbarY + span); + float ratio = (top - scrollbarY) / span; + scroll = -scrollbarMaxScroll * AnimationUtility.clamp(ratio, 0f, 1f); + } + boolean press(float mx, float my, int button) { + if (button != 0 || !isHovered(mx, my)) return false; + draggingScrollbar = true; + scrollbarDragOffset = ClickGuiMath.insideRect(mx, my, scrollbarX, scrollbarThumbY, scrollbarW, scrollbarThumbH) ? my - scrollbarThumbY : scrollbarThumbH * 0.5f; + scrollToMouse(my); + return true; + } + void release(int button) { if (button == 0) draggingScrollbar = false; } + void dragIfNeeded() { if (draggingScrollbar) scrollToMouse(ClickGuiRenderer.getMouseY()); } + void renderScrollbar(float x, float y, float w, float h, float maxScroll, float scale, SettingsGuiPalette palette, float opacity) { + updateMetrics(x, y, w, h, maxScroll, scale); + if (!scrollbarVisible) return; + if (isHovered(ClickGuiRenderer.getMouseX(), ClickGuiRenderer.getMouseY()) || draggingScrollbar) SystemCursor.set(SystemCursor.CursorType.SCROLL); + LayoutRender2D.roundedQuad(scrollbarX, scrollbarY, scrollbarW, scrollbarH, 2f * scale, fade(palette.panelScrollTrackA(), opacity), fade(palette.panelScrollTrackB(), opacity), fade(palette.panelScrollTrackB(), opacity), fade(palette.panelScrollTrackA(), opacity)); + LayoutRender2D.roundedQuad(scrollbarX, scrollbarThumbY, scrollbarW, scrollbarThumbH, 2f * scale, fade(palette.panelScrollHandleA(), opacity), fade(palette.panelScrollHandleB(), opacity), fade(palette.panelScrollHandleB(), opacity), fade(palette.panelScrollHandleA(), opacity)); + } + static int fade(int c, float o) { return LayoutRender2D.alpha(c, AnimationUtility.clamp01(o)); } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/theme/ThemeComponent.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/theme/ThemeComponent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/theme/ThemeComponent.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/theme/ThemeComponent.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/theme/ThemeEditorPreviewComponent.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/theme/ThemeEditorPreviewComponent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/theme/ThemeEditorPreviewComponent.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/theme/ThemeEditorPreviewComponent.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/render/LayoutRender2D.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/render/LayoutRender2D.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/render/LayoutRender2D.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/render/LayoutRender2D.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/ClickGuiPickerIconManager.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/ClickGuiPickerIconManager.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/ClickGuiPickerIconManager.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/ClickGuiPickerIconManager.java diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/ClickGuiPickerState.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/ClickGuiPickerState.java new file mode 100644 index 0000000..59ddee0 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/ClickGuiPickerState.java @@ -0,0 +1,166 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.clickgui.picker; + +import dev.loki.lovisual.features.gui.clickgui.picker.catalog.PickerCatalog; +import dev.loki.lovisual.features.gui.clickgui.picker.catalog.PickerCatalogFactory; +import dev.loki.lovisual.features.gui.clickgui.picker.catalog.PickerEntryData; +import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; +import dev.loki.lovisual.features.gui.clickgui.settings.owner.PickerDetailOwner; +import dev.loki.lovisual.features.gui.clickgui.settings.text.list.TextListSetting; +import dev.loki.lovisual.features.theme.Theme; +import dev.loki.lovisual.features.theme.Themes; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import java.util.*; + +public final class ClickGuiPickerState { + static final float SCALE = 2f; + static final float WINDOW_W = 340f * SCALE; + static final float WINDOW_H = 260f * SCALE; + static final float PADDING = 8f * SCALE; + static final float HEADER_H = 28f * SCALE; + static final float SEARCH_H = 15f * SCALE; + static final float FILTER_H = SEARCH_H; + static final float SEARCH_W = 98f * SCALE; + static final float FILTER_W = 80f * SCALE; + static final float CLOSE_SIZE = 12f * SCALE; + static final float CARD_SIZE = 38f * SCALE; + static final float CARD_GAP = 5f * SCALE; + static final float SCROLL_STEP = 15f * SCALE; + static final float MARQUEE_SPEED = 18f * SCALE; + static final float MARQUEE_GAP = 10f * SCALE; + static final float MARQUEE_PAUSE_SEC = 0.55f; + + final TextListSetting owner; + final TextListSetting.PickerMode mode; + final PickerCatalog catalog; + final String title; + final StringBuilder search = new StringBuilder(); + final List allEntries = new ArrayList<>(); + final List visibleAll = new ArrayList<>(); + final List visibleSelected = new ArrayList<>(); + final Map byId = new LinkedHashMap<>(); + final List cardHits = new ArrayList<>(); + boolean showAll = true; + boolean listening; + float modeAnim; + float scroll; + float smoothScroll; + boolean draggingScrollbar; + boolean scrollbarVisible; + float sbTrackX, sbTrackY, sbTrackW, sbTrackH, sbThumbH, contentHeight; + float windowX, windowY, searchX, searchY, filterX, filterY, closeX, closeY; + float gridX, gridY, gridW, gridH, detailX, detailY, detailW, detailH; + String focusedId; + TextRenderer interBold; + String hoveredCardId; + long hoveredCardStartMs; + float allFilterHoverAnim, selectedFilterHoverAnim; + + public ClickGuiPickerState(TextListSetting owner, String title, TextListSetting.PickerMode mode) { + this.owner = owner; + this.mode = mode == null ? TextListSetting.PickerMode.TEXT : mode; + this.catalog = PickerCatalogFactory.forMode(this.mode); + this.title = title == null || title.isBlank() ? "Select" : title; + this.reloadEntries(); + this.refreshFiltered(true); + } + + static boolean inside(float mx, float my, float x, float y, float w, float h) { + return mx >= x && mx <= x + w && my >= y && my <= y + h; + } + + public boolean isListening() { return listening; } + public void toggleListening() { listening = !listening; } + public void stopListening() { listening = false; } + + public void insertChar(char c) { + if (Character.isISOControl(c)) return; + search.append(c); refreshFiltered(true); + } + public void backspace() { + if (search.length() <= 0) return; + search.deleteCharAt(search.length() - 1); refreshFiltered(true); + } + + public void handleMouseMove(float mx, float my) { + if (!draggingScrollbar) return; + ClickGuiPickerLogic.layout(this, ClickGuiRenderer.framebufferWidth(), ClickGuiRenderer.framebufferHeight()); + ClickGuiPickerLogic.scrollToPosition(this, my); + } + + public void handleMouseDown(float mx, float my, int button) { + if (button != 0) return; + ClickGuiPickerLogic.layout(this, ClickGuiRenderer.framebufferWidth(), ClickGuiRenderer.framebufferHeight()); + if (!inside(mx, my, windowX, windowY, windowW(), WINDOW_H)) { ClickGuiRenderer.closePickerScreen(); return; } + if (inside(mx, my, closeX, closeY, CLOSE_SIZE, CLOSE_SIZE)) { ClickGuiRenderer.closePickerScreen(); return; } + if (inside(mx, my, searchX, searchY, SEARCH_W, SEARCH_H)) { listening = true; return; } + listening = false; + if (inside(mx, my, filterX, filterY, FILTER_W, FILTER_H)) { + boolean nextShowAll = mx < filterX + FILTER_W * 0.5f; + if (showAll != nextShowAll) { showAll = nextShowAll; scroll = 0f; smoothScroll = 0f; draggingScrollbar = false; ClickGuiPickerLogic.clampScroll(this); } + return; + } + PickerDetailOwner d = detailOwner(); + if (d != null && inside(mx, my, detailX, detailY, detailW, detailH) && d.mouseClickedPickerDetails(mx, my, button)) return; + if (ClickGuiPickerLogic.handleScrollbarMouseDown(this, mx, my)) return; + for (CardHit hit : cardHits) { + if (!inside(mx, my, hit.x(), hit.y(), hit.w(), hit.h())) continue; + focusedId = hit.getId(); + if (d != null) { d.onPickerFocusChanged(hit.getId()); if (!d.shouldToggleSelectionOnCardClick()) return; } + ClickGuiPickerLogic.toggleSelection(this, hit.getId()); return; + } + } + + public void handleMouseUp(float mx, float my, int button) { + PickerDetailOwner d = detailOwner(); + if (d != null) d.mouseReleasedPickerDetails(mx, my, button); + if (button == 0) draggingScrollbar = false; + } + + public void scroll(double delta) { + ClickGuiPickerLogic.layout(this, ClickGuiRenderer.framebufferWidth(), ClickGuiRenderer.framebufferHeight()); + PickerDetailOwner d = detailOwner(); + if (d != null) { + float mx = ClickGuiRenderer.getMouseX(); float my = ClickGuiRenderer.getMouseY(); + if (inside(mx, my, detailX, detailY, detailW, detailH) && d.mouseScrolledPickerDetails(mx, my, delta)) return; + } + scroll += (float) (delta * SCROLL_STEP); ClickGuiPickerLogic.clampScroll(this); + } + + public void render(int fbw, int fbh) { + ClickGuiPickerLogic.layout(this, fbw, fbh); ClickGuiPickerLogic.clampScroll(this); + float targetMode = showAll ? 0f : 1f; + modeAnim = AnimationUtility.approach(modeAnim, targetMode, 0.22f); modeAnim = AnimationUtility.snap(modeAnim, targetMode, 0.01f); + smoothScroll = AnimationUtility.approach(smoothScroll, scroll, 0.2f); smoothScroll = AnimationUtility.snap(smoothScroll, scroll, 0.05f); + float mx = ClickGuiRenderer.getMouseX(); float my = ClickGuiRenderer.getMouseY(); + SettingsGuiPalette p = SettingsGuiPalette.current(); Themes.Theme t = Theme.theme(); + ClickGuiPickerChromeRenderer.drawWindow(this, p); + ClickGuiPickerChromeRenderer.drawHeader(this, p); + ClickGuiPickerChromeRenderer.drawCloseButton(this, mx, my, p, t); + ClickGuiPickerFilterRenderer.drawFilterPill(this, mx, my, p, t); + ClickGuiPickerChromeRenderer.drawSearchField(this, mx, my, p, t); + ClickGuiPickerGridRenderer.drawGrid(this, mx, my, p, t); + drawPickerDetails(mx, my); + ClickGuiPickerGridRenderer.drawScrollbar(this, mx, my, p); + } + + float windowW() { PickerDetailOwner d = detailOwner(); return WINDOW_W + (d != null ? d.pickerDetailWidth() : 0f); } + PickerDetailOwner detailOwner() { return owner instanceof PickerDetailOwner d ? d : null; } + void drawPickerDetails(float mx, float my) { + PickerDetailOwner d = detailOwner(); if (d == null) return; + d.renderPickerDetails(detailX, detailY, detailW, detailH, focusedId, mx, my); + } + + private void reloadEntries() { ClickGuiPickerLogic.reloadEntries(this); } + private void refreshFiltered(boolean reset) { ClickGuiPickerLogic.refreshFiltered(this, reset); } + + record CardHit(String id, float x, float y, float w, float h) { public String getId() { return id; } } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/catalog/PickerCatalog.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/catalog/PickerCatalog.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/catalog/PickerCatalog.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/catalog/PickerCatalog.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/catalog/PickerCatalogFactory.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/catalog/PickerCatalogFactory.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/catalog/PickerCatalogFactory.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/catalog/PickerCatalogFactory.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/catalog/PickerEntryData.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/catalog/PickerEntryData.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/catalog/PickerEntryData.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/catalog/PickerEntryData.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/screen/ClickGuiPickerScreen.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/screen/ClickGuiPickerScreen.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/screen/ClickGuiPickerScreen.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/screen/ClickGuiPickerScreen.java diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/state/ClickGuiPickerChromeRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/state/ClickGuiPickerChromeRenderer.java new file mode 100644 index 0000000..afafb66 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/state/ClickGuiPickerChromeRenderer.java @@ -0,0 +1,81 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.clickgui.picker; + +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.LayoutRender2D; +import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; +import dev.loki.lovisual.features.theme.Themes; +import dev.loki.lovisual.render.engine.text.font.Fonts; +import dev.loki.lovisual.render.engine.text.font.info.FontInfo; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.render.helpers.clip.ScissorFunction; + +final class ClickGuiPickerChromeRenderer { + private ClickGuiPickerChromeRenderer() {} + + static void drawWindow(ClickGuiPickerState s, SettingsGuiPalette p) { + ClickGuiRenderer.drawBlur(s.windowX, s.windowY, s.windowW(), ClickGuiPickerState.WINDOW_H, 8f * ClickGuiPickerState.SCALE, 0xFF000000, 200f / 255f); + LayoutRender2D.roundedSoftShadow(s.windowX, s.windowY, s.windowW(), ClickGuiPickerState.WINDOW_H, 8f * ClickGuiPickerState.SCALE, 8f * ClickGuiPickerState.SCALE, 0.018f, p.menuShadow()); + LayoutRender2D.roundedQuad(s.windowX, s.windowY, s.windowW(), ClickGuiPickerState.WINDOW_H, 8f * ClickGuiPickerState.SCALE, p.menuWindowBgLeft(), p.menuWindowBgRight(), p.menuWindowBgRight(), p.menuWindowBgLeft()); + LayoutRender2D.roundedStroke(s.windowX, s.windowY, s.windowW(), ClickGuiPickerState.WINDOW_H, 8f * ClickGuiPickerState.SCALE, 0.1f * ClickGuiPickerState.SCALE, p.menuWindowStroke()); + } + + static void drawHeader(ClickGuiPickerState s, SettingsGuiPalette p) { + float sepY = s.windowY + ClickGuiPickerState.HEADER_H; + LayoutRender2D.rectQuad(s.windowX + ClickGuiPickerState.PADDING, sepY, s.windowW() - ClickGuiPickerState.PADDING * 2f, 0.5f * ClickGuiPickerState.SCALE, p.menuLineStrong(), p.menuLineMid(), p.menuLineMid(), p.menuLineStrong()); + ClickGuiRenderer.drawText(ClickGuiRenderer.getInterRegular(), s.title, s.windowX + ClickGuiPickerState.PADDING, s.windowY + 7f * ClickGuiPickerState.SCALE, 11f * ClickGuiPickerState.SCALE, p.menuHeaderText(), false); + String count = s.owner.getValueSet().size() + " selected"; + ClickGuiRenderer.drawText(ClickGuiRenderer.getInterRegular(), count, s.windowX + ClickGuiPickerState.PADDING, s.windowY + 15.5f * ClickGuiPickerState.SCALE, 8f * ClickGuiPickerState.SCALE, p.panelMuted(), false); + } + + static void drawCloseButton(ClickGuiPickerState s, float mx, float my, SettingsGuiPalette p, Themes.Theme t) { + boolean hover = ClickGuiPickerState.inside(mx, my, s.closeX, s.closeY, ClickGuiPickerState.CLOSE_SIZE, ClickGuiPickerState.CLOSE_SIZE); + int base = hover ? p.panelPillActive() : p.panelPillBase(); + LayoutRender2D.rounded(s.closeX, s.closeY, ClickGuiPickerState.CLOSE_SIZE, ClickGuiPickerState.CLOSE_SIZE, 3.5f * ClickGuiPickerState.SCALE, base); + LayoutRender2D.roundedStroke(s.closeX, s.closeY, ClickGuiPickerState.CLOSE_SIZE, ClickGuiPickerState.CLOSE_SIZE, 3.5f * ClickGuiPickerState.SCALE, 0.5f * ClickGuiPickerState.SCALE, p.panelStroke()); + float pad = 3f * ClickGuiPickerState.SCALE; + float x1 = s.closeX + pad; float y1 = s.closeY + pad; + float x2 = s.closeX + ClickGuiPickerState.CLOSE_SIZE - pad; float y2 = s.closeY + ClickGuiPickerState.CLOSE_SIZE - pad; + int c = hover ? t.accent() : p.panelText(); + ClickGuiRenderer.drawLine(x1, y1, x2, y2, c); + ClickGuiRenderer.drawLine(x2, y1, x1, y2, c); + } + + static void drawSearchField(ClickGuiPickerState s, float mx, float my, SettingsGuiPalette p, Themes.Theme th) { + boolean hover = ClickGuiPickerState.inside(mx, my, s.searchX, s.searchY, ClickGuiPickerState.SEARCH_W, ClickGuiPickerState.SEARCH_H); + boolean active = s.listening; + int bgL = p.panelBgLeft(); int bgR = p.panelBgRight(); + if (hover) { bgL = ClickGuiRenderer.mixColor(bgL, p.panelPillActive(), 0.18f); bgR = ClickGuiRenderer.mixColor(bgR, p.panelPillActive(), 0.18f); } + if (active) { bgL = ClickGuiRenderer.mixColor(bgL, th.accentSoft(), 0.16f); bgR = ClickGuiRenderer.mixColor(bgR, th.accentSoft(), 0.16f); } + ClickGuiRenderer.drawBlur(s.searchX, s.searchY, ClickGuiPickerState.SEARCH_W, ClickGuiPickerState.SEARCH_H, 3f * ClickGuiPickerState.SCALE, p.panelBlurTint(), 170f / 255f); + LayoutRender2D.roundedQuad(s.searchX, s.searchY, ClickGuiPickerState.SEARCH_W, ClickGuiPickerState.SEARCH_H, 3f * ClickGuiPickerState.SCALE, bgL, bgR, bgR, bgL); + LayoutRender2D.roundedStroke(s.searchX, s.searchY, ClickGuiPickerState.SEARCH_W, ClickGuiPickerState.SEARCH_H, 3f * ClickGuiPickerState.SCALE, 0.5f * ClickGuiPickerState.SCALE, p.panelStroke()); + float dividerX = s.searchX + ClickGuiPickerState.SEARCH_W - 13f * ClickGuiPickerState.SCALE; + LayoutRender2D.rect(dividerX, s.searchY + 3f * ClickGuiPickerState.SCALE, 0.5f * ClickGuiPickerState.SCALE, ClickGuiPickerState.SEARCH_H - 6f * ClickGuiPickerState.SCALE, SettingsGuiPalette.withAlpha(p.panelMuted(), 95)); + TextRenderer font = ClickGuiRenderer.getInterRegular(); + float textSize = 7.5f * ClickGuiPickerState.SCALE; + boolean empty = s.search.length() == 0; + boolean caret = active && dev.loki.lovisual.render.engine.animation.AnimationUtility.blink(500L); + String shown = empty && !active ? "Search" : s.search.toString(); + if (active && caret) shown += "|"; + int color = empty && !active ? p.panelMuted() : p.panelText(); + float textY = s.searchY + (ClickGuiPickerState.SEARCH_H - ClickGuiRenderer.textHeight(font, textSize)) * 0.5f; + float textX = s.searchX + 4f * ClickGuiPickerState.SCALE; + boolean clipped = ScissorFunction.pushRaw(textX, s.searchY + ClickGuiPickerState.SCALE, Math.max(1f, dividerX - textX - 1.5f * ClickGuiPickerState.SCALE), ClickGuiPickerState.SEARCH_H - 2f * ClickGuiPickerState.SCALE); + ClickGuiRenderer.drawText(font, shown, textX, textY, textSize, color, false); + if (clipped) ScissorFunction.pop(); + TextRenderer icons = Fonts.renderer("Icons", FontInfo.Type.Regular, ClickGuiRenderer.getInterRegular()); + String icon = "s"; float iconSize = 8f * ClickGuiPickerState.SCALE; + float iconW = ClickGuiRenderer.textWidth(icons, icon, iconSize); float iconH = ClickGuiRenderer.textHeight(icons, iconSize); + float iconX = dividerX + ((s.searchX + ClickGuiPickerState.SEARCH_W) - dividerX - iconW) * 0.5f; + float iconY = s.searchY + (ClickGuiPickerState.SEARCH_H - iconH) * 0.5f - 0.35f * ClickGuiPickerState.SCALE; + int iconColor = active ? p.panelText() : p.panelMuted(); + ClickGuiRenderer.drawText(icons, icon, iconX, iconY, iconSize, iconColor, false); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/state/ClickGuiPickerFilterRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/state/ClickGuiPickerFilterRenderer.java new file mode 100644 index 0000000..7106028 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/state/ClickGuiPickerFilterRenderer.java @@ -0,0 +1,102 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.clickgui.picker; + +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.LayoutRender2D; +import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; +import dev.loki.lovisual.features.theme.Themes; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.render.helpers.clip.ScissorFunction; +import net.minecraft.util.Util; + +final class ClickGuiPickerFilterRenderer { + private ClickGuiPickerFilterRenderer() {} + + static void drawFilterPill(ClickGuiPickerState s, float mx, float my, SettingsGuiPalette p, Themes.Theme th) { + boolean hover = ClickGuiPickerState.inside(mx, my, s.filterX, s.filterY, ClickGuiPickerState.FILTER_W, ClickGuiPickerState.FILTER_H); + boolean allHover = hover && mx < s.filterX + ClickGuiPickerState.FILTER_W * 0.5f; + boolean selHover = hover && !allHover; + float dt = AnimationUtility.deltaTime(); + s.allFilterHoverAnim = AnimationUtility.snap(AnimationUtility.approach(s.allFilterHoverAnim, allHover ? 1f : 0f, dt, 12f), allHover ? 1f : 0f, 0.01f); + s.selectedFilterHoverAnim = AnimationUtility.snap(AnimationUtility.approach(s.selectedFilterHoverAnim, selHover ? 1f : 0f, dt, 12f), selHover ? 1f : 0f, 0.01f); + int baseLeft = SettingsGuiPalette.mix(p.panelPillBase(), p.panelBgLeft(), 0.34f); + int baseRight = SettingsGuiPalette.mix(p.panelPillBase(), p.panelBgRight(), 0.46f); + if (hover) { + float ha = Math.max(s.allFilterHoverAnim, s.selectedFilterHoverAnim); + baseLeft = SettingsGuiPalette.mix(baseLeft, p.menuCategoryHoverLeft(), 0.18f * ha); + baseRight = SettingsGuiPalette.mix(baseRight, p.menuCategoryHoverRight(), 0.18f * ha); + } + ClickGuiRenderer.drawBlur(s.filterX, s.filterY, ClickGuiPickerState.FILTER_W, ClickGuiPickerState.FILTER_H, 4f * ClickGuiPickerState.SCALE, p.panelBlurTint(), 150f / 255f); + LayoutRender2D.roundedQuad(s.filterX, s.filterY, ClickGuiPickerState.FILTER_W, ClickGuiPickerState.FILTER_H, 3.5f * ClickGuiPickerState.SCALE, baseLeft, baseRight, baseRight, baseLeft); + LayoutRender2D.roundedStrokeQuad(s.filterX, s.filterY, ClickGuiPickerState.FILTER_W, ClickGuiPickerState.FILTER_H, 3.5f * ClickGuiPickerState.SCALE, 0.5f * ClickGuiPickerState.SCALE, + SettingsGuiPalette.mix(p.panelStroke(), p.moduleDividerStart(), 0.20f), SettingsGuiPalette.mix(p.panelStroke(), p.moduleDividerEnd(), 0.20f), + SettingsGuiPalette.mix(p.panelStroke(), p.moduleDividerEnd(), 0.20f), SettingsGuiPalette.mix(p.panelStroke(), p.moduleDividerStart(), 0.20f)); + float pad = 1.2f * ClickGuiPickerState.SCALE; + float half = (ClickGuiPickerState.FILTER_W - pad * 2f) * 0.5f; + float activeX = s.filterX + pad + half * s.modeAnim; + float activeHover = s.showAll ? s.allFilterHoverAnim : s.selectedFilterHoverAnim; + int activeA = SettingsGuiPalette.mix(p.panelPillActive(), SettingsGuiPalette.withAlpha(th.accentSoft(), 230), 0.18f + activeHover * 0.10f); + int activeB = SettingsGuiPalette.mix(p.panelPillActive(), SettingsGuiPalette.withAlpha(th.accent(), 235), 0.12f + activeHover * 0.14f); + int activeC = SettingsGuiPalette.mix(SettingsGuiPalette.darken(p.panelPillActive(), 0.18f), SettingsGuiPalette.withAlpha(th.accentSoft(), 215), 0.14f + activeHover * 0.10f); + LayoutRender2D.roundedQuad(activeX, s.filterY + pad, half, ClickGuiPickerState.FILTER_H - pad * 2f, 3f * ClickGuiPickerState.SCALE, activeA, activeB, activeC, activeA); + LayoutRender2D.roundedStrokeQuad(activeX, s.filterY + pad, half, ClickGuiPickerState.FILTER_H - pad * 2f, 3f * ClickGuiPickerState.SCALE, 0.35f * ClickGuiPickerState.SCALE, + SettingsGuiPalette.withAlpha(th.accentSoft(), 155), SettingsGuiPalette.withAlpha(th.accent(), 165), + SettingsGuiPalette.withAlpha(th.accentSoft(), 135), SettingsGuiPalette.withAlpha(th.accentSoft(), 155)); + drawFilterDivider(s, s.filterX + pad + half, p, th); + TextRenderer font = ClickGuiRenderer.getInterRegular(); + float textSize = 7.5f * ClickGuiPickerState.SCALE; + float allW = ClickGuiRenderer.textWidth(font, "All", textSize); float allH = ClickGuiRenderer.textHeight(font, textSize); + float selW = ClickGuiRenderer.textWidth(font, "Selected", textSize); float selH = ClickGuiRenderer.textHeight(font, textSize); + float allCx = s.filterX + half * 0.5f + pad; float selCx = s.filterX + half + half * 0.5f + pad; + float cy = s.filterY + (ClickGuiPickerState.FILTER_H - allH) * 0.5f; + int allColor = s.showAll ? p.panelText() : SettingsGuiPalette.mix(p.panelMuted(), p.panelText(), 0.22f * s.allFilterHoverAnim); + int selColor = !s.showAll ? p.panelText() : SettingsGuiPalette.mix(p.panelMuted(), p.panelText(), 0.22f * s.selectedFilterHoverAnim); + ClickGuiRenderer.drawText(font, "All", allCx - allW * 0.5f, cy, textSize, allColor, false); + ClickGuiRenderer.drawText(font, "Selected", selCx - selW * 0.5f, s.filterY + (ClickGuiPickerState.FILTER_H - selH) * 0.5f, textSize, selColor, false); + } + + static void drawFilterDivider(ClickGuiPickerState s, float x, SettingsGuiPalette p, Themes.Theme th) { + float backingW = Math.max(1.25f * ClickGuiPickerState.SCALE, 1.05f); + float backingH = ClickGuiPickerState.FILTER_H - 4.4f * ClickGuiPickerState.SCALE; + float backingY = s.filterY + (ClickGuiPickerState.FILTER_H - backingH) * 0.5f; + int backingTop = SettingsGuiPalette.withAlpha(p.panelBgLeft(), 104); + int backingBottom = SettingsGuiPalette.withAlpha(p.panelBgRight(), 84); + LayoutRender2D.roundedQuad(x - backingW * 0.5f, backingY, backingW, backingH, backingW * 0.5f, backingTop, backingTop, backingBottom, backingBottom); + float lineW = Math.max(0.5f * ClickGuiPickerState.SCALE, 0.75f); + float lineH = ClickGuiPickerState.FILTER_H - 6.0f * ClickGuiPickerState.SCALE; + float lineY = s.filterY + (ClickGuiPickerState.FILTER_H - lineH) * 0.5f; + int top = SettingsGuiPalette.withAlpha(SettingsGuiPalette.mix(p.moduleDividerStart(), th.accentSoft(), 0.30f), 154); + int mid = SettingsGuiPalette.withAlpha(SettingsGuiPalette.mix(p.moduleDividerEnd(), th.accent(), 0.34f), 214); + int bottom = SettingsGuiPalette.withAlpha(SettingsGuiPalette.mix(p.moduleDividerEnd(), p.panelText(), 0.22f), 142); + LayoutRender2D.roundedQuad(x - lineW * 0.5f, lineY, lineW, lineH, lineW * 0.5f, top, mid, bottom, top); + } + + static void drawCardLabel(ClickGuiPickerState s, TextRenderer font, String label, float cardX, float textY, float maxW, float size, int color, boolean hover) { + String text = label == null ? "" : label; + float fullW = ClickGuiRenderer.textWidth(font, text, size); + if (!hover || fullW <= maxW + 0.5f * ClickGuiPickerState.SCALE) { + String shown = ClickGuiRenderer.fitText(font, text, size, maxW); + float shownW = ClickGuiRenderer.textWidth(font, shown, size); + ClickGuiRenderer.drawText(font, shown, cardX + (ClickGuiPickerState.CARD_SIZE - shownW) * 0.5f, textY, size, color, false); + return; + } + float clipX = cardX + (ClickGuiPickerState.CARD_SIZE - maxW) * 0.5f; + float textH = Math.max(1f, ClickGuiRenderer.textHeight(font, size)); + boolean clipped = ScissorFunction.pushRaw(clipX, textY - ClickGuiPickerState.SCALE, maxW, textH + 2f * ClickGuiPickerState.SCALE); + float elapsed = Math.max(0f, (Util.getMillis() - s.hoveredCardStartMs) / 1000f); + float cycleDistance = fullW + ClickGuiPickerState.MARQUEE_GAP; + float cycleTime = ClickGuiPickerState.MARQUEE_PAUSE_SEC + cycleDistance / Math.max(1f, ClickGuiPickerState.MARQUEE_SPEED); + float t = elapsed % cycleTime; + float offset = t <= ClickGuiPickerState.MARQUEE_PAUSE_SEC ? 0f : -(t - ClickGuiPickerState.MARQUEE_PAUSE_SEC) * ClickGuiPickerState.MARQUEE_SPEED; + ClickGuiRenderer.drawText(font, text, clipX + offset, textY, size, color, false); + if (cycleDistance > maxW * 0.5f) ClickGuiRenderer.drawText(font, text, clipX + offset + cycleDistance, textY, size, color, false); + if (clipped) ScissorFunction.pop(); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/state/ClickGuiPickerGridRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/state/ClickGuiPickerGridRenderer.java new file mode 100644 index 0000000..12c2bbf --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/state/ClickGuiPickerGridRenderer.java @@ -0,0 +1,120 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.clickgui.picker; + +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.LayoutRender2D; +import dev.loki.lovisual.features.gui.clickgui.picker.catalog.PickerEntryData; +import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; +import dev.loki.lovisual.features.theme.Themes; +import dev.loki.lovisual.render.engine.text.font.FontFace; +import dev.loki.lovisual.render.engine.text.font.Fonts; +import dev.loki.lovisual.render.engine.text.font.faces.FontFamily; +import dev.loki.lovisual.render.engine.text.font.info.FontInfo; +import dev.loki.lovisual.render.engine.text.glyph.CustomTextRenderer; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.render.helpers.clip.ScissorFunction; +import dev.loki.lovisual.render.helpers.util.SystemCursor; +import java.util.List; +import java.util.Set; +import net.minecraft.util.Util; +import net.minecraft.world.item.ItemStack; + +final class ClickGuiPickerGridRenderer { + private ClickGuiPickerGridRenderer() {} + + static void drawGrid(ClickGuiPickerState s, float mx, float my, SettingsGuiPalette p, Themes.Theme th) { + List visible = ClickGuiPickerLogic.visibleEntries(s); + Set selected = s.owner.getValueSet(); + int columns = ClickGuiPickerLogic.columns(s); + s.cardHits.clear(); + ClickGuiRenderer.beginPickerIconScissor(s.gridX, s.gridY, s.gridW, s.gridH); + boolean clipped = ScissorFunction.pushRaw(s.gridX, s.gridY, s.gridW, s.gridH); + float step = ClickGuiPickerState.CARD_SIZE + ClickGuiPickerState.CARD_GAP; + float iconScale = 1.55f * ClickGuiPickerState.SCALE; + float iconPx = 16f * iconScale; + TextRenderer font = ClickGuiRenderer.getInterRegular(); + float nameSize = 5.9f * ClickGuiPickerState.SCALE; + boolean hasHovered = false; + for (int i = 0; i < visible.size(); i++) { + int col = i % columns; int row = i / columns; + float cardX = s.gridX + col * step; float cardY = s.gridY + row * step + s.smoothScroll; + if (cardY + ClickGuiPickerState.CARD_SIZE < s.gridY - 0.5f * ClickGuiPickerState.SCALE) continue; + if (cardY > s.gridY + s.gridH + 0.5f * ClickGuiPickerState.SCALE) continue; + PickerEntryData entry = visible.get(i); + boolean isSelected = selected.contains(entry.getId()); + boolean hover = ClickGuiPickerState.inside(mx, my, cardX, cardY, ClickGuiPickerState.CARD_SIZE, ClickGuiPickerState.CARD_SIZE); + if (hover) { hasHovered = true; if (!entry.getId().equals(s.hoveredCardId)) { s.hoveredCardId = entry.getId(); s.hoveredCardStartMs = Util.getMillis(); } } + int left = SettingsGuiPalette.withAlpha(SettingsGuiPalette.mix(th.windowBg(), th.surface(), 0.42f), 205); + int right = SettingsGuiPalette.withAlpha(SettingsGuiPalette.darken(SettingsGuiPalette.mix(th.windowHeader(), th.windowBg(), 0.55f), 0.26f), 205); + if (isSelected) { left = ClickGuiRenderer.mixColor(left, th.accentSoft(), 0.14f); right = ClickGuiRenderer.mixColor(right, th.accentSoft(), 0.26f); } + if (hover) { left = ClickGuiRenderer.mixColor(left, p.moduleCardTopStrong(), 0.40f); right = ClickGuiRenderer.mixColor(right, p.moduleCardBottomStrong(), 0.42f); } + LayoutRender2D.roundedQuad(cardX, cardY, ClickGuiPickerState.CARD_SIZE, ClickGuiPickerState.CARD_SIZE, 4f * ClickGuiPickerState.SCALE, left, right, right, left); + int stroke = isSelected ? th.accentSoft() : p.moduleDividerStart(); + float strokeW = isSelected ? 0.8f * ClickGuiPickerState.SCALE : 0.45f * ClickGuiPickerState.SCALE; + LayoutRender2D.roundedStroke(cardX, cardY, ClickGuiPickerState.CARD_SIZE, ClickGuiPickerState.CARD_SIZE, 4f * ClickGuiPickerState.SCALE, strokeW, stroke); + float lineY = cardY + ClickGuiPickerState.CARD_SIZE - 11f * ClickGuiPickerState.SCALE; + LayoutRender2D.rectQuad(cardX + 3f * ClickGuiPickerState.SCALE, lineY, ClickGuiPickerState.CARD_SIZE - 6f * ClickGuiPickerState.SCALE, 0.45f * ClickGuiPickerState.SCALE, p.moduleDividerStart(), p.moduleDividerEnd(), p.moduleDividerEnd(), p.moduleDividerStart()); + ItemStack stack = entry.stack(); + if (stack != null && !stack.isEmpty()) { + float iconX = cardX + (ClickGuiPickerState.CARD_SIZE - iconPx) * 0.5f; float iconY = cardY + 4f * ClickGuiPickerState.SCALE; + float clipX = cardX + ClickGuiPickerState.SCALE; float clipY = cardY + ClickGuiPickerState.SCALE; + float clipW = ClickGuiPickerState.CARD_SIZE - 2f * ClickGuiPickerState.SCALE; + float clipH = Math.max(1f, lineY - clipY - 0.75f * ClickGuiPickerState.SCALE); + ClickGuiRenderer.queuePickerIcon(stack, iconX, iconY, iconScale, clipX, clipY, clipW, clipH); + } else { + TextRenderer bold = interBold(s); + float qSize = 13f * ClickGuiPickerState.SCALE; String q = "?"; + float qW = ClickGuiRenderer.textWidth(bold, q, qSize); float qH = ClickGuiRenderer.textHeight(bold, qSize); + float iconAreaTop = cardY + 3.5f * ClickGuiPickerState.SCALE; float iconAreaBottom = lineY - ClickGuiPickerState.SCALE; + float qX = cardX + (ClickGuiPickerState.CARD_SIZE - qW) * 0.5f; float qY = iconAreaTop + ((iconAreaBottom - iconAreaTop) - qH) * 0.5f; + int qColor = isSelected ? SettingsGuiPalette.withAlpha(p.moduleTitleText(), 210) : SettingsGuiPalette.withAlpha(p.panelMuted(), 190); + ClickGuiRenderer.drawText(bold, q, qX, qY, qSize, qColor, false); + } + float textMaxW = ClickGuiPickerState.CARD_SIZE - 6f * ClickGuiPickerState.SCALE; + float textY = lineY + 1.5f * ClickGuiPickerState.SCALE; + int textColor = isSelected ? p.moduleTitleText() : p.moduleDescriptionText(); + ClickGuiPickerFilterRenderer.drawCardLabel(s, font, entry.label(), cardX, textY, textMaxW, nameSize, textColor, hover); + s.cardHits.add(new ClickGuiPickerState.CardHit(entry.getId(), cardX, cardY, ClickGuiPickerState.CARD_SIZE, ClickGuiPickerState.CARD_SIZE)); + } + if (!hasHovered) { s.hoveredCardId = null; s.hoveredCardStartMs = 0L; } + if (clipped) ScissorFunction.pop(); + ClickGuiRenderer.endPickerIconScissor(); + if (visible.isEmpty()) { + ClickGuiRenderer.drawText(ClickGuiRenderer.getInterRegular(), s.showAll ? "No entries" : "Nothing selected", s.gridX + 4f * ClickGuiPickerState.SCALE, s.gridY + 4f * ClickGuiPickerState.SCALE, 7.2f * ClickGuiPickerState.SCALE, p.panelMuted(), false); + } + } + + static void drawScrollbar(ClickGuiPickerState s, float mx, float my, SettingsGuiPalette p) { + if (!s.scrollbarVisible || s.contentHeight <= s.gridH + 0.5f) { s.scrollbarVisible = false; return; } + float baseW = 2.25f * ClickGuiPickerState.SCALE; + boolean hover = ClickGuiPickerLogic.isScrollbarHovered(s, mx, my) || s.draggingScrollbar; + if (hover) SystemCursor.set(SystemCursor.CursorType.SCROLL); + float trackW = hover ? 3.2f * ClickGuiPickerState.SCALE : baseW; + float trackX = s.gridX + s.gridW - trackW - ClickGuiPickerState.SCALE; + float trackY = s.gridY + ClickGuiPickerState.SCALE; + float trackH = Math.max(12f * ClickGuiPickerState.SCALE, s.gridH - 2f * ClickGuiPickerState.SCALE); + s.sbTrackX = trackX; s.sbTrackY = trackY; s.sbTrackW = trackW; s.sbTrackH = trackH; + LayoutRender2D.roundedQuad(trackX, trackY, trackW, trackH, 2f * ClickGuiPickerState.SCALE, p.moduleScrollTrackA(), p.moduleScrollTrackB(), p.moduleScrollTrackB(), p.moduleScrollTrackA()); + float thumbH = Math.max(16f * ClickGuiPickerState.SCALE, trackH * (s.gridH / s.contentHeight)); + s.sbThumbH = thumbH; + float ratio = s.contentHeight <= s.gridH ? 0f : (-s.smoothScroll / (s.contentHeight - s.gridH)); + float thumbY = trackY + (trackH - thumbH) * ClickGuiPickerLogic.clamp(ratio, 0f, 1f); + LayoutRender2D.roundedQuad(trackX, thumbY, trackW, thumbH, 2f * ClickGuiPickerState.SCALE, p.moduleScrollHandleA(), p.moduleScrollHandleB(), p.moduleScrollHandleB(), p.moduleScrollHandleA()); + } + + private static TextRenderer interBold(ClickGuiPickerState s) { + if (s.interBold != null) return s.interBold; + FontFamily family = Fonts.getFamily("Inter"); + if (family == null) { s.interBold = ClickGuiRenderer.getInterRegular(); return s.interBold; } + FontFace face = family.get(FontInfo.Type.Bold); + if (face == null) { s.interBold = ClickGuiRenderer.getInterRegular(); return s.interBold; } + s.interBold = new CustomTextRenderer(face); + return s.interBold; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/state/ClickGuiPickerLogic.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/state/ClickGuiPickerLogic.java new file mode 100644 index 0000000..dbe2ac5 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/picker/state/ClickGuiPickerLogic.java @@ -0,0 +1,140 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.clickgui.picker; + +import dev.loki.lovisual.features.gui.clickgui.picker.catalog.PickerEntryData; +import java.util.*; +import net.minecraft.world.item.ItemStack; + +final class ClickGuiPickerLogic { + static final Comparator ENTRY_ORDER = Comparator + .comparing(PickerEntryData::label, String.CASE_INSENSITIVE_ORDER) + .thenComparing(PickerEntryData::id, String.CASE_INSENSITIVE_ORDER); + + private ClickGuiPickerLogic() {} + + static void reloadEntries(ClickGuiPickerState s) { + s.allEntries.clear(); + s.allEntries.addAll(s.catalog.entries(s.owner)); + } + + static void refreshFiltered(ClickGuiPickerState s, boolean resetScroll) { + String needle = s.search.toString().trim().toLowerCase(Locale.ROOT); + s.byId.clear(); + for (PickerEntryData e : s.allEntries) s.byId.put(e.getId(), e); + s.visibleAll.clear(); + for (PickerEntryData e : s.allEntries) if (matches(e, needle)) s.visibleAll.add(e); + s.visibleAll.sort(ENTRY_ORDER); + s.visibleSelected.clear(); + for (String id : s.owner.getValueSet()) { + PickerEntryData e = s.byId.get(id); + if (e == null) e = new PickerEntryData(id, id, ItemStack.EMPTY); + if (!matches(e, needle)) continue; + s.visibleSelected.add(e); + } + s.visibleSelected.sort(ENTRY_ORDER); + if (resetScroll) { + s.scroll = 0f; + s.smoothScroll = 0f; + s.draggingScrollbar = false; + } else clampScroll(s); + } + + static List visibleEntries(ClickGuiPickerState s) { + return s.showAll ? s.visibleAll : s.visibleSelected; + } + + static void toggleSelection(ClickGuiPickerState s, String id) { + Set next = s.owner.getValueSet(); + if (next.contains(id)) next.remove(id); + else next.add(s.owner.normalizeEntry(id)); + s.owner.setValueSet(next); + if (!next.contains(s.focusedId)) { + s.focusedId = next.isEmpty() ? null : next.iterator().next(); + var d = s.detailOwner(); + if (d != null && s.focusedId != null) d.onPickerFocusChanged(s.focusedId); + } + refreshFiltered(s, false); + } + + static boolean matches(PickerEntryData e, String needle) { + if (needle == null || needle.isBlank()) return true; + String id = e.getId() == null ? "" : e.getId().toLowerCase(Locale.ROOT); + String label = e.label() == null ? "" : e.label().toLowerCase(Locale.ROOT); + return id.contains(needle) || label.contains(needle); + } + + static void layout(ClickGuiPickerState s, int fbw, int fbh) { + s.windowX = (fbw - s.windowW()) * 0.5f; + s.windowY = (fbh - ClickGuiPickerState.WINDOW_H) * 0.5f; + s.closeX = s.windowX + s.windowW() - ClickGuiPickerState.PADDING - ClickGuiPickerState.CLOSE_SIZE; + s.closeY = s.windowY + 7f * ClickGuiPickerState.SCALE; + s.searchX = s.closeX - 3f * ClickGuiPickerState.SCALE - ClickGuiPickerState.SEARCH_W; + s.searchY = s.windowY + 6f * ClickGuiPickerState.SCALE; + s.filterX = s.searchX - 3f * ClickGuiPickerState.SCALE - ClickGuiPickerState.FILTER_W; + s.filterY = s.searchY; + s.gridX = s.windowX + ClickGuiPickerState.PADDING; + s.gridY = s.windowY + ClickGuiPickerState.HEADER_H + 6f * ClickGuiPickerState.SCALE; + var d = s.detailOwner(); + s.detailW = d != null ? d.pickerDetailWidth() : 0f; + s.detailY = s.gridY; + s.detailH = ClickGuiPickerState.WINDOW_H - (s.gridY - s.windowY) - ClickGuiPickerState.PADDING; + s.detailX = s.windowX + s.windowW() - ClickGuiPickerState.PADDING - s.detailW; + s.gridW = s.windowW() - ClickGuiPickerState.PADDING * 2f - (d != null ? s.detailW + ClickGuiPickerState.CARD_GAP : 0f); + s.gridH = s.detailH; + } + + static int columns(ClickGuiPickerState s) { + return Math.max(1, (int) Math.floor((s.gridW + ClickGuiPickerState.CARD_GAP) / (ClickGuiPickerState.CARD_SIZE + ClickGuiPickerState.CARD_GAP))); + } + + static float computeContentHeight(ClickGuiPickerState s, int count) { + if (count <= 0) return 0f; + int cols = columns(s); + int rows = (count + cols - 1) / cols; + return rows * ClickGuiPickerState.CARD_SIZE + Math.max(0, rows - 1) * ClickGuiPickerState.CARD_GAP; + } + + static void clampScroll(ClickGuiPickerState s) { + s.contentHeight = computeContentHeight(s, visibleEntries(s).size()); + float min = Math.min(0f, s.gridH - s.contentHeight); + s.scroll = clamp(s.scroll, min, 0f); + s.smoothScroll = clamp(s.smoothScroll, min, 0f); + s.scrollbarVisible = s.contentHeight > s.gridH + 0.5f; + } + + static boolean handleScrollbarMouseDown(ClickGuiPickerState s, float mx, float my) { + if (!s.scrollbarVisible) return false; + if (!isScrollbarHovered(s, mx, my)) return false; + s.draggingScrollbar = true; + scrollToPosition(s, my); + return true; + } + + static boolean isScrollbarHovered(ClickGuiPickerState s, float mx, float my) { + if (!s.scrollbarVisible) return false; + float pad = 3f * ClickGuiPickerState.SCALE; + return mx >= s.sbTrackX - pad && mx <= s.sbTrackX + s.sbTrackW + pad && my >= s.sbTrackY - pad && my <= s.sbTrackY + s.sbTrackH + pad; + } + + static void scrollToPosition(ClickGuiPickerState s, float my) { + if (s.contentHeight <= s.gridH + 0.5f) { s.scroll = 0f; s.smoothScroll = 0f; return; } + float span = s.sbTrackH - s.sbThumbH; + if (span <= 0f) return; + float rel = my - s.sbTrackY - s.sbThumbH * 0.5f; + float t = clamp(rel / span, 0f, 1f); + float range = s.contentHeight - s.gridH; + s.scroll = -range * t; + s.smoothScroll = s.scroll; + } + + static float clamp(float v, float min, float max) { + if (v < min) return min; + return Math.min(v, max); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/protocol/CombatProtocolHeuristicsEditorState.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/protocol/CombatProtocolHeuristicsEditorState.java new file mode 100644 index 0000000..d80d5db --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/protocol/CombatProtocolHeuristicsEditorState.java @@ -0,0 +1,79 @@ +package dev.loki.lovisual.features.gui.clickgui.protocol; + +import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; +import dev.loki.lovisual.features.gui.clickgui.settings.cooldown.ProtocolHeuristicsSetting; +import dev.loki.lovisual.util.combat.protocol.heuristics.CombatProtocolHeuristicSource; +import dev.loki.lovisual.util.combat.protocol.heuristics.CombatProtocolHeuristicsConfig; +import org.lwjgl.glfw.GLFW; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +public final class CombatProtocolHeuristicsEditorState { + static final float SCALE = 2f; + static final float WINDOW_W = 420f * SCALE; + static final float WINDOW_H = 280f * SCALE; + static final float PADDING = 9f * SCALE; + static final float HEADER_H = 31f * SCALE; + static final float SOURCE_W = 108f * SCALE; + static final float GAP = 7f * SCALE; + static final float CLOSE_SIZE = 12f * SCALE; + + final ProtocolHeuristicsSetting owner; + final String title; + final List sourceHits = new ArrayList<>(); + final EnumMap sourceHover = new EnumMap<>(CombatProtocolHeuristicSource.class); + final EnumMap sourceSelect = new EnumMap<>(CombatProtocolHeuristicSource.class); + final EnumMap> editors = new EnumMap<>(CombatProtocolHeuristicSource.class); + + CombatProtocolHeuristicSource selected = CombatProtocolHeuristicSource.BOSSBAR; + ProtocolInlineEditor focusedEditor; + float windowX, windowY, openAnim; + Rect close = Rect.EMPTY; + Rect reset = Rect.EMPTY; + float closeHover, resetHover; + + public CombatProtocolHeuristicsEditorState(ProtocolHeuristicsSetting owner, String title) { + this.owner = owner; + this.title = title == null || title.isBlank() ? tr("title", "Protocol heuristics") : title; + CombatProtocolHeuristicsConfig config = CombatProtocolHeuristicsConfig.get(); + for (CombatProtocolHeuristicSource source : CombatProtocolHeuristicSource.values()) { + EnumMap byFamily = new EnumMap<>(CombatProtocolHeuristicsConfig.ProtocolFamily.class); + for (CombatProtocolHeuristicsConfig.ProtocolFamily family : CombatProtocolHeuristicsConfig.ProtocolFamily.values()) byFamily.put(family, new ProtocolInlineEditor(source, family, config.patterns(source, family))); + editors.put(source, byFamily); + sourceHover.put(source, 0f); + sourceSelect.put(source, source == selected ? 1f : 0f); + } + } + + public void handleMouseMove(float mx, float my) { CombatProtocolInputHandler.handleMouseMove(this, mx, my); } + public void handleMouseDown(float mx, float my, int button) { CombatProtocolInputHandler.handleMouseDown(this, mx, my, button); } + public void handleMouseUp(float mx, float my, int button) { CombatProtocolInputHandler.handleMouseUp(this, mx, my, button); } + public void scroll(double delta) { CombatProtocolInputHandler.handleScroll(this, delta); } + public boolean charTyped(char chr) { return CombatProtocolInputHandler.charTyped(this, chr); } + public boolean keyPressed(int key, int scancode, int action, int modifiers) { return CombatProtocolInputHandler.keyPressed(this, key, scancode, action, modifiers); } + public void render(int width, int height) { CombatProtocolRenderer.render(this, width, height); } + + void focus(ProtocolInlineEditor editor) { if (focusedEditor == editor) return; if (focusedEditor != null) focusedEditor.setFocused(false); focusedEditor = editor; if (focusedEditor != null) focusedEditor.setFocused(true); } + void blurEditor() { if (focusedEditor != null) focusedEditor.setFocused(false); focusedEditor = null; } + void reloadEditors(CombatProtocolHeuristicSource source) { CombatProtocolHeuristicsConfig config = CombatProtocolHeuristicsConfig.get(); for (CombatProtocolHeuristicsConfig.ProtocolFamily family : CombatProtocolHeuristicsConfig.ProtocolFamily.values()) editor(source, family).replace(config.patterns(source, family)); } + ProtocolInlineEditor editor(CombatProtocolHeuristicSource s, CombatProtocolHeuristicsConfig.ProtocolFamily f) { return editors.get(s).get(f); } + boolean sourceEnabled(CombatProtocolHeuristicSource s) { return owner.sources() != null && owner.sources().get(s.key()); } + + static boolean inside(float mx, float my, float x, float y, float w, float h) { return mx >= x && mx <= x + w && my >= y && my <= y + h; } + static int clamp(int v, int min, int max) { return Math.max(min, Math.min(max, v)); } + static float clamp(float v, float min, float max) { return Math.max(min, Math.min(max, v)); } + static boolean validPattern(String p) { if (p == null || p.isBlank()) return false; try { Pattern.compile(p, Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE); return true; } catch (PatternSyntaxException ignored) { return false; } } + static LineColumn lineColumn(int index, String[] lines) { int rem = clamp(index, 0, totalLength(lines)); for (int l = 0; l < lines.length; l++) { int len = lines[l].length(); if (rem <= len) return new LineColumn(l, rem); rem -= len + 1; } int last = Math.max(0, lines.length - 1); return new LineColumn(last, lines[last].length()); } + static int indexForLineColumn(String[] lines, int line, int column) { int idx = 0; int rl = clamp(line, 0, lines.length - 1); for (int i = 0; i < rl; i++) idx += lines[i].length() + 1; return idx + clamp(column, 0, lines[rl].length()); } + static int totalLength(String[] lines) { int len = 0; for (int i = 0; i < lines.length; i++) { len += lines[i].length(); if (i < lines.length - 1) len++; } return len; } + static boolean shiftDown() { long w = net.minecraft.client.Minecraft.getInstance().getWindow().handle(); return GLFW.glfwGetKey(w, GLFW.GLFW_KEY_LEFT_SHIFT) == GLFW.GLFW_PRESS || GLFW.glfwGetKey(w, GLFW.GLFW_KEY_RIGHT_SHIFT) == GLFW.GLFW_PRESS; } + static String tr(String suffix, String fallback, Object... args) { return dev.loki.lovisual.features.gui.clickgui.util.ClickGuiI18n.tr("clickgui.protocol_heuristics." + suffix, fallback, args); } + + record SourceHit(CombatProtocolHeuristicSource source, Rect card) {} + record LineColumn(int line, int column) {} + record Rect(float x, float y, float w, float h) { static final Rect EMPTY = new Rect(0f,0f,0f,0f); boolean contains(float mx,float my){ return inside(mx,my,x,y,w,h);} } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/protocol/state/CombatProtocolInputHandler.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/protocol/state/CombatProtocolInputHandler.java new file mode 100644 index 0000000..9523978 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/protocol/state/CombatProtocolInputHandler.java @@ -0,0 +1,68 @@ +package dev.loki.lovisual.features.gui.clickgui.protocol; + +import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; +import dev.loki.lovisual.util.combat.protocol.heuristics.CombatProtocolHeuristicSource; +import dev.loki.lovisual.util.combat.protocol.heuristics.CombatProtocolHeuristicsConfig; +import org.lwjgl.glfw.GLFW; + +final class CombatProtocolInputHandler { + private CombatProtocolInputHandler() {} + + static void handleMouseMove(CombatProtocolHeuristicsEditorState s, float mx, float my) { + if (s.focusedEditor != null) s.focusedEditor.mouseMoved(mx, my); + } + + static void handleMouseDown(CombatProtocolHeuristicsEditorState s, float mx, float my, int button) { + if (button != GLFW.GLFW_MOUSE_BUTTON_LEFT && button != GLFW.GLFW_MOUSE_BUTTON_RIGHT) return; + if (!CombatProtocolHeuristicsEditorState.inside(mx, my, s.windowX, s.windowY, CombatProtocolHeuristicsEditorState.WINDOW_W, CombatProtocolHeuristicsEditorState.WINDOW_H) || s.close.contains(mx, my)) { + ClickGuiRenderer.closeProtocolHeuristicsEditor(); + return; + } + for (CombatProtocolHeuristicsEditorState.SourceHit hit : s.sourceHits) { + if (!hit.card().contains(mx, my)) continue; + s.blurEditor(); + s.selected = hit.source(); + boolean enabled = s.sourceEnabled(hit.source()); + if (button == GLFW.GLFW_MOUSE_BUTTON_LEFT) s.owner.setSourceEnabled(hit.source().key(), !enabled); + return; + } + ProtocolInlineEditor legacy = s.editor(s.selected, CombatProtocolHeuristicsConfig.ProtocolFamily.LEGACY); + ProtocolInlineEditor modern = s.editor(s.selected, CombatProtocolHeuristicsConfig.ProtocolFamily.MODERN); + if (legacy.mousePressed(mx, my, button)) { s.focus(legacy); return; } + if (modern.mousePressed(mx, my, button)) { s.focus(modern); return; } + if (button == GLFW.GLFW_MOUSE_BUTTON_LEFT && s.reset.contains(mx, my)) { + s.blurEditor(); + CombatProtocolHeuristicsConfig.get().reset(s.selected); + s.reloadEditors(s.selected); + return; + } + if (button == GLFW.GLFW_MOUSE_BUTTON_LEFT) s.blurEditor(); + } + + static void handleMouseUp(CombatProtocolHeuristicsEditorState s, float mx, float my, int button) { + if (s.focusedEditor != null) s.focusedEditor.mouseReleased(button); + } + + static void handleScroll(CombatProtocolHeuristicsEditorState s, double delta) { + float mx = ClickGuiRenderer.getMouseX(); + float my = ClickGuiRenderer.getMouseY(); + ProtocolInlineEditor legacy = s.editor(s.selected, CombatProtocolHeuristicsConfig.ProtocolFamily.LEGACY); + ProtocolInlineEditor modern = s.editor(s.selected, CombatProtocolHeuristicsConfig.ProtocolFamily.MODERN); + if (legacy.scroll(mx, my, delta)) return; + modern.scroll(mx, my, delta); + } + + static boolean charTyped(CombatProtocolHeuristicsEditorState s, char chr) { + return s.focusedEditor != null && s.focusedEditor.charTyped(chr); + } + + static boolean keyPressed(CombatProtocolHeuristicsEditorState s, int key, int scancode, int action, int modifiers) { + if (action != GLFW.GLFW_PRESS && action != GLFW.GLFW_REPEAT) return s.focusedEditor != null; + if (s.focusedEditor != null) { + if (key == GLFW.GLFW_KEY_ESCAPE) { s.blurEditor(); return true; } + return s.focusedEditor.keyPressed(key, modifiers); + } + if (key == GLFW.GLFW_KEY_ESCAPE) { ClickGuiRenderer.closeProtocolHeuristicsEditor(); return true; } + return true; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/protocol/state/CombatProtocolRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/protocol/state/CombatProtocolRenderer.java new file mode 100644 index 0000000..07a2fd2 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/protocol/state/CombatProtocolRenderer.java @@ -0,0 +1,183 @@ +package dev.loki.lovisual.features.gui.clickgui.protocol; + +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; +import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; +import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiI18n; +import dev.loki.lovisual.features.theme.Theme; +import dev.loki.lovisual.features.theme.Themes; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.render.helpers.util.SystemCursor; +import dev.loki.lovisual.util.combat.protocol.heuristics.CombatProtocolHeuristicSource; +import dev.loki.lovisual.util.combat.protocol.heuristics.CombatProtocolHeuristicsConfig; + +final class CombatProtocolRenderer { + private CombatProtocolRenderer() {} + + static void render(CombatProtocolHeuristicsEditorState s, int width, int height) { + float dt = AnimationUtility.deltaTime(); + s.openAnim = AnimationUtility.snap(AnimationUtility.approach(s.openAnim, 1f, dt, 8.5f), 1f, 0.008f); + float eased = easeOutCubic(s.openAnim); + s.windowX = (width - CombatProtocolHeuristicsEditorState.WINDOW_W) * 0.5f; + s.windowY = (height - CombatProtocolHeuristicsEditorState.WINDOW_H) * 0.5f + (1f - eased) * 10f * CombatProtocolHeuristicsEditorState.SCALE; + SettingsGuiPalette palette = SettingsGuiPalette.current(); + Themes.Theme theme = Theme.theme(); + TextRenderer regular = ClickGuiRenderer.getInterRegular(); + TextRenderer medium = ClickGuiRenderer.getInterMedium(); + float mx = ClickGuiRenderer.getMouseX(); + float my = ClickGuiRenderer.getMouseY(); + float previousAlpha = ClickGuiRenderer.setRenderAlphaMultiplier(eased); + try { + int shadow = SettingsGuiPalette.withAlpha(palette.menuShadow(), Math.round(255f * eased)); + ClickGuiRenderer.drawBlur(s.windowX, s.windowY, CombatProtocolHeuristicsEditorState.WINDOW_W, CombatProtocolHeuristicsEditorState.WINDOW_H, 9f * CombatProtocolHeuristicsEditorState.SCALE, 0xFF000000, 210f / 255f); + ClickGuiRenderer.drawRoundedRectShadow(s.windowX, s.windowY, CombatProtocolHeuristicsEditorState.WINDOW_W, CombatProtocolHeuristicsEditorState.WINDOW_H, 9f * CombatProtocolHeuristicsEditorState.SCALE, 13f * CombatProtocolHeuristicsEditorState.SCALE, 1.5f * CombatProtocolHeuristicsEditorState.SCALE, shadow); + ClickGuiRenderer.drawRoundedRectGradient(s.windowX, s.windowY, CombatProtocolHeuristicsEditorState.WINDOW_W, CombatProtocolHeuristicsEditorState.WINDOW_H, 9f * CombatProtocolHeuristicsEditorState.SCALE, palette.menuWindowBgLeft(), palette.menuWindowBgRight(), 0f); + ClickGuiRenderer.drawRoundedRectStrokeGradient(s.windowX, s.windowY, CombatProtocolHeuristicsEditorState.WINDOW_W, CombatProtocolHeuristicsEditorState.WINDOW_H, 9f * CombatProtocolHeuristicsEditorState.SCALE, 0.55f * CombatProtocolHeuristicsEditorState.SCALE, SettingsGuiPalette.mix(palette.menuWindowStroke(), theme.accentSoft(), 0.18f), SettingsGuiPalette.mix(palette.menuWindowStroke(), theme.accent(), 0.10f), 0f); + renderHeader(s, regular, medium, mx, my, palette, theme); + float contentY = s.windowY + CombatProtocolHeuristicsEditorState.HEADER_H + CombatProtocolHeuristicsEditorState.PADDING; + float contentH = CombatProtocolHeuristicsEditorState.WINDOW_H - CombatProtocolHeuristicsEditorState.HEADER_H - CombatProtocolHeuristicsEditorState.PADDING * 2f; + renderSources(s, s.windowX + CombatProtocolHeuristicsEditorState.PADDING, contentY, CombatProtocolHeuristicsEditorState.SOURCE_W, contentH, regular, medium, mx, my, palette, theme); + float detailsX = s.windowX + CombatProtocolHeuristicsEditorState.PADDING + CombatProtocolHeuristicsEditorState.SOURCE_W + CombatProtocolHeuristicsEditorState.GAP; + float detailsW = CombatProtocolHeuristicsEditorState.WINDOW_W - CombatProtocolHeuristicsEditorState.PADDING * 2f - CombatProtocolHeuristicsEditorState.SOURCE_W - CombatProtocolHeuristicsEditorState.GAP; + renderDetails(s, detailsX, contentY, detailsW, contentH, regular, medium, mx, my, palette, theme); + } finally { + ClickGuiRenderer.restoreRenderAlphaMultiplier(previousAlpha); + } + } + + private static void renderHeader(CombatProtocolHeuristicsEditorState s, TextRenderer regular, TextRenderer medium, float mx, float my, SettingsGuiPalette palette, Themes.Theme theme) { + float accentX = s.windowX + CombatProtocolHeuristicsEditorState.PADDING; + float accentY = s.windowY + 7f * CombatProtocolHeuristicsEditorState.SCALE; + ClickGuiRenderer.drawRoundedRect(accentX, accentY, 2f * CombatProtocolHeuristicsEditorState.SCALE, 17f * CombatProtocolHeuristicsEditorState.SCALE, CombatProtocolHeuristicsEditorState.SCALE, theme.accent()); + ClickGuiRenderer.drawRoundedRectGlow(accentX, accentY, 2f * CombatProtocolHeuristicsEditorState.SCALE, 17f * CombatProtocolHeuristicsEditorState.SCALE, CombatProtocolHeuristicsEditorState.SCALE, 7f * CombatProtocolHeuristicsEditorState.SCALE, SettingsGuiPalette.withAlpha(theme.accent(), 90)); + ClickGuiRenderer.drawText(medium, s.title, accentX + 7f * CombatProtocolHeuristicsEditorState.SCALE, s.windowY + 5.5f * CombatProtocolHeuristicsEditorState.SCALE, 10.5f * CombatProtocolHeuristicsEditorState.SCALE, palette.menuHeaderText(), false); + String subtitle = tr("subtitle", "Configure protocol signals independently for each source"); + ClickGuiRenderer.drawText(regular, subtitle, accentX + 7f * CombatProtocolHeuristicsEditorState.SCALE, s.windowY + 17f * CombatProtocolHeuristicsEditorState.SCALE, 6.3f * CombatProtocolHeuristicsEditorState.SCALE, palette.panelMuted(), false); + float separatorY = s.windowY + CombatProtocolHeuristicsEditorState.HEADER_H; + ClickGuiRenderer.drawGradientRect(s.windowX + CombatProtocolHeuristicsEditorState.PADDING, separatorY, CombatProtocolHeuristicsEditorState.WINDOW_W - CombatProtocolHeuristicsEditorState.PADDING * 2f, 0.5f * CombatProtocolHeuristicsEditorState.SCALE, palette.menuLineStrong(), palette.menuLineMid(), palette.menuLineMid(), palette.menuLineStrong()); + s.close = new CombatProtocolHeuristicsEditorState.Rect(s.windowX + CombatProtocolHeuristicsEditorState.WINDOW_W - CombatProtocolHeuristicsEditorState.PADDING - CombatProtocolHeuristicsEditorState.CLOSE_SIZE, s.windowY + 9f * CombatProtocolHeuristicsEditorState.SCALE, CombatProtocolHeuristicsEditorState.CLOSE_SIZE, CombatProtocolHeuristicsEditorState.CLOSE_SIZE); + s.closeHover = animate(s.closeHover, s.close.contains(mx, my), 14f); + int bg = SettingsGuiPalette.mix(palette.panelPillBase(), palette.panelPillActive(), 0.22f + s.closeHover * 0.58f); + ClickGuiRenderer.drawRoundedRect(s.close.x(), s.close.y(), s.close.w(), s.close.h(), 3.5f * CombatProtocolHeuristicsEditorState.SCALE, bg); + ClickGuiRenderer.drawRoundedRectStroke(s.close.x(), s.close.y(), s.close.w(), s.close.h(), 3.5f * CombatProtocolHeuristicsEditorState.SCALE, 0.45f * CombatProtocolHeuristicsEditorState.SCALE, SettingsGuiPalette.mix(palette.panelStroke(), theme.accentSoft(), s.closeHover * 0.45f)); + float pad = 3.2f * CombatProtocolHeuristicsEditorState.SCALE; + int color = SettingsGuiPalette.mix(palette.panelMuted(), theme.accent(), s.closeHover); + ClickGuiRenderer.drawLine(s.close.x() + pad, s.close.y() + pad, s.close.x() + s.close.w() - pad, s.close.y() + s.close.h() - pad, color); + ClickGuiRenderer.drawLine(s.close.x() + s.close.w() - pad, s.close.y() + pad, s.close.x() + pad, s.close.y() + s.close.h() - pad, color); + if (s.close.contains(mx, my)) SystemCursor.set(SystemCursor.CursorType.HAND); + } + + private static void renderSources(CombatProtocolHeuristicsEditorState s, float x, float y, float w, float h, TextRenderer regular, TextRenderer medium, float mx, float my, SettingsGuiPalette palette, Themes.Theme theme) { + s.sourceHits.clear(); + panel(x, y, w, h, palette); + ClickGuiRenderer.drawText(medium, tr("sources", "Sources"), x + 8f * CombatProtocolHeuristicsEditorState.SCALE, y + 7f * CombatProtocolHeuristicsEditorState.SCALE, 7.2f * CombatProtocolHeuristicsEditorState.SCALE, palette.panelText(), false); + ClickGuiRenderer.drawText(regular, tr("source_controls", "LMB toggle \u00b7 RMB configure"), x + 8f * CombatProtocolHeuristicsEditorState.SCALE, y + 16f * CombatProtocolHeuristicsEditorState.SCALE, 5.3f * CombatProtocolHeuristicsEditorState.SCALE, palette.panelMuted(), false); + float rowY = y + 28f * CombatProtocolHeuristicsEditorState.SCALE; + float rowH = 39f * CombatProtocolHeuristicsEditorState.SCALE; + float rowGap = 5f * CombatProtocolHeuristicsEditorState.SCALE; + for (CombatProtocolHeuristicSource source : CombatProtocolHeuristicSource.values()) { + CombatProtocolHeuristicsEditorState.Rect card = new CombatProtocolHeuristicsEditorState.Rect(x + 5f * CombatProtocolHeuristicsEditorState.SCALE, rowY, w - 10f * CombatProtocolHeuristicsEditorState.SCALE, rowH); + boolean hovered = card.contains(mx, my); + boolean active = source == s.selected; + boolean enabled = s.sourceEnabled(source); + float hover = animate(s.sourceHover.get(source), hovered, 13f); + float select = animate(s.sourceSelect.get(source), active, 11f); + s.sourceHover.put(source, hover); + s.sourceSelect.put(source, select); + int baseLeft = SettingsGuiPalette.mix(palette.panelPillBase(), palette.menuCategoryHoverLeft(), hover * 0.30f); + int baseRight = SettingsGuiPalette.mix(palette.panelPillBase(), palette.menuCategoryHoverRight(), hover * 0.30f); + float accentMix = Math.max(enabled ? 0.42f : 0f, select * 0.32f); + int left = SettingsGuiPalette.mix(baseLeft, theme.accentSoft(), accentMix); + int right = SettingsGuiPalette.mix(baseRight, theme.accent(), accentMix * 0.72f); + ClickGuiRenderer.drawRoundedRectGradient(card.x(), card.y(), card.w(), card.h(), 5.5f * CombatProtocolHeuristicsEditorState.SCALE, left, right, 0f); + ClickGuiRenderer.drawRoundedRectStrokeGradient(card.x(), card.y(), card.w(), card.h(), 5.5f * CombatProtocolHeuristicsEditorState.SCALE, 0.5f * CombatProtocolHeuristicsEditorState.SCALE, SettingsGuiPalette.mix(palette.panelStroke(), theme.accentSoft(), Math.max(select, enabled ? 0.55f : 0f)), SettingsGuiPalette.mix(palette.panelStroke(), theme.accent(), Math.max(select * 0.7f, enabled ? 0.32f : 0f)), 0f); + float markerW = (1.5f + 1.5f * select) * CombatProtocolHeuristicsEditorState.SCALE; + int marker = enabled ? theme.accent() : SettingsGuiPalette.withAlpha(palette.panelMuted(), 100); + ClickGuiRenderer.drawRoundedRect(card.x() + 3f * CombatProtocolHeuristicsEditorState.SCALE, card.y() + 7f * CombatProtocolHeuristicsEditorState.SCALE, markerW, card.h() - 14f * CombatProtocolHeuristicsEditorState.SCALE, markerW * 0.5f, marker); + if (enabled) ClickGuiRenderer.drawRoundedRectGlow(card.x() + 3f * CombatProtocolHeuristicsEditorState.SCALE, card.y() + 7f * CombatProtocolHeuristicsEditorState.SCALE, markerW, card.h() - 14f * CombatProtocolHeuristicsEditorState.SCALE, markerW * 0.5f, 5f * CombatProtocolHeuristicsEditorState.SCALE, SettingsGuiPalette.withAlpha(theme.accent(), Math.round(55 + 55 * hover))); + String name = sourceName(source); + ClickGuiRenderer.drawText(medium, name, card.x() + 9f * CombatProtocolHeuristicsEditorState.SCALE, card.y() + 7f * CombatProtocolHeuristicsEditorState.SCALE, 7f * CombatProtocolHeuristicsEditorState.SCALE, palette.panelText(), false); + String state = enabled ? tr("enabled", "Enabled") : tr("disabled", "Disabled"); + int stateColor = enabled ? theme.accent() : palette.panelMuted(); + ClickGuiRenderer.drawText(regular, state, card.x() + 9f * CombatProtocolHeuristicsEditorState.SCALE, card.y() + 20f * CombatProtocolHeuristicsEditorState.SCALE, 5.8f * CombatProtocolHeuristicsEditorState.SCALE, stateColor, false); + int count = s.editor(source, CombatProtocolHeuristicsConfig.ProtocolFamily.LEGACY).patternCount() + s.editor(source, CombatProtocolHeuristicsConfig.ProtocolFamily.MODERN).patternCount(); + String countLabel = tr("rules_short", "%s rules", count); + float countSize = 5.3f * CombatProtocolHeuristicsEditorState.SCALE; + float countW = ClickGuiRenderer.textWidth(regular, countLabel, countSize); + ClickGuiRenderer.drawText(regular, countLabel, card.x() + card.w() - 7f * CombatProtocolHeuristicsEditorState.SCALE - countW, card.y() + 20f * CombatProtocolHeuristicsEditorState.SCALE, countSize, palette.panelMuted(), false); + s.sourceHits.add(new CombatProtocolHeuristicsEditorState.SourceHit(source, card)); + if (hovered) SystemCursor.set(SystemCursor.CursorType.HAND); + rowY += rowH + rowGap; + } + } + + private static void renderDetails(CombatProtocolHeuristicsEditorState s, float x, float y, float w, float h, TextRenderer regular, TextRenderer medium, float mx, float my, SettingsGuiPalette palette, Themes.Theme theme) { + panel(x, y, w, h, palette); + boolean enabled = s.sourceEnabled(s.selected); + ClickGuiRenderer.drawText(medium, sourceName(s.selected), x + 8f * CombatProtocolHeuristicsEditorState.SCALE, y + 6f * CombatProtocolHeuristicsEditorState.SCALE, 8.2f * CombatProtocolHeuristicsEditorState.SCALE, palette.panelText(), false); + String sourceHint = tr("source_hint." + s.selected.key(), defaultSourceHint(s.selected)); + ClickGuiRenderer.drawText(regular, sourceHint, x + 8f * CombatProtocolHeuristicsEditorState.SCALE, y + 17f * CombatProtocolHeuristicsEditorState.SCALE, 5.8f * CombatProtocolHeuristicsEditorState.SCALE, palette.panelMuted(), false); + String status = enabled ? tr("enabled", "Enabled") : tr("disabled", "Disabled"); + float statusSize = 5.7f * CombatProtocolHeuristicsEditorState.SCALE; + float statusW = ClickGuiRenderer.textWidth(medium, status, statusSize) + 12f * CombatProtocolHeuristicsEditorState.SCALE; + float statusX = x + w - 8f * CombatProtocolHeuristicsEditorState.SCALE - statusW; + int statusBg = enabled ? SettingsGuiPalette.withAlpha(SettingsGuiPalette.mix(theme.accentSoft(), palette.panelPillActive(), 0.32f), 205) : palette.panelPillBase(); + ClickGuiRenderer.drawRoundedRect(statusX, y + 7f * CombatProtocolHeuristicsEditorState.SCALE, statusW, 11f * CombatProtocolHeuristicsEditorState.SCALE, 5.5f * CombatProtocolHeuristicsEditorState.SCALE, statusBg); + ClickGuiRenderer.drawText(medium, status, statusX + 6f * CombatProtocolHeuristicsEditorState.SCALE, y + 9.6f * CombatProtocolHeuristicsEditorState.SCALE, statusSize, enabled ? theme.accent() : palette.panelMuted(), false); + float headingH = 29f * CombatProtocolHeuristicsEditorState.SCALE; + float footerH = 19f * CombatProtocolHeuristicsEditorState.SCALE; + float editorGap = 6f * CombatProtocolHeuristicsEditorState.SCALE; + float editorH = (h - headingH - footerH - editorGap * 2f) * 0.5f; + float editorY = y + headingH; + s.editor(s.selected, CombatProtocolHeuristicsConfig.ProtocolFamily.LEGACY).render(x + 6f * CombatProtocolHeuristicsEditorState.SCALE, editorY, w - 12f * CombatProtocolHeuristicsEditorState.SCALE, editorH, regular, medium, mx, my, palette, theme); + editorY += editorH + editorGap; + s.editor(s.selected, CombatProtocolHeuristicsConfig.ProtocolFamily.MODERN).render(x + 6f * CombatProtocolHeuristicsEditorState.SCALE, editorY, w - 12f * CombatProtocolHeuristicsEditorState.SCALE, editorH, regular, medium, mx, my, palette, theme); + String resetLabel = tr("reset_source", "Reset source"); + float resetW = Math.max(58f * CombatProtocolHeuristicsEditorState.SCALE, ClickGuiRenderer.textWidth(regular, resetLabel, 5.8f * CombatProtocolHeuristicsEditorState.SCALE) + 14f * CombatProtocolHeuristicsEditorState.SCALE); + s.reset = new CombatProtocolHeuristicsEditorState.Rect(x + w - 6f * CombatProtocolHeuristicsEditorState.SCALE - resetW, y + h - 15f * CombatProtocolHeuristicsEditorState.SCALE, resetW, 11f * CombatProtocolHeuristicsEditorState.SCALE); + s.resetHover = animate(s.resetHover, s.reset.contains(mx, my), 13f); + button(s.reset, resetLabel, s.resetHover, regular, palette, theme); + if (s.reset.contains(mx, my)) SystemCursor.set(SystemCursor.CursorType.HAND); + String hint = s.focusedEditor == null ? tr("edit_hint", "Click a pattern area to edit") : tr("keyboard_hint", "Ctrl+A/C/X/V \u00b7 Enter new line \u00b7 Esc unfocus"); + ClickGuiRenderer.drawText(regular, hint, x + 8f * CombatProtocolHeuristicsEditorState.SCALE, y + h - 12.3f * CombatProtocolHeuristicsEditorState.SCALE, 5.2f * CombatProtocolHeuristicsEditorState.SCALE, palette.panelMuted(), false); + } + + static void panel(float x, float y, float w, float h, SettingsGuiPalette palette) { + ClickGuiRenderer.drawBlur(x, y, w, h, 5f * CombatProtocolHeuristicsEditorState.SCALE, palette.panelBlurTint(), 175f / 255f); + ClickGuiRenderer.drawRoundedRectGradient(x, y, w, h, 6f * CombatProtocolHeuristicsEditorState.SCALE, palette.panelBgLeft(), palette.panelBgRight(), 0f); + ClickGuiRenderer.drawRoundedRectStroke(x, y, w, h, 6f * CombatProtocolHeuristicsEditorState.SCALE, 0.5f * CombatProtocolHeuristicsEditorState.SCALE, palette.panelStroke()); + } + + static void button(CombatProtocolHeuristicsEditorState.Rect rect, String label, float hover, TextRenderer font, SettingsGuiPalette palette, Themes.Theme theme) { + int bg = SettingsGuiPalette.mix(palette.panelPillBase(), theme.accentSoft(), 0.08f + hover * 0.24f); + ClickGuiRenderer.drawRoundedRect(rect.x(), rect.y(), rect.w(), rect.h(), 3.5f * CombatProtocolHeuristicsEditorState.SCALE, bg); + ClickGuiRenderer.drawRoundedRectStroke(rect.x(), rect.y(), rect.w(), rect.h(), 3.5f * CombatProtocolHeuristicsEditorState.SCALE, 0.4f * CombatProtocolHeuristicsEditorState.SCALE, SettingsGuiPalette.mix(palette.panelStroke(), theme.accent(), hover * 0.42f)); + float size = 5.8f * CombatProtocolHeuristicsEditorState.SCALE; + float tw = ClickGuiRenderer.textWidth(font, label, size); + float th = ClickGuiRenderer.textHeight(font, size); + ClickGuiRenderer.drawText(font, label, rect.x() + (rect.w() - tw) * 0.5f, rect.y() + (rect.h() - th) * 0.5f, size, SettingsGuiPalette.mix(palette.panelText(), theme.accent(), hover * 0.55f), false); + } + + static float animate(float value, boolean active, float speed) { + float target = active ? 1f : 0f; + return AnimationUtility.snap(AnimationUtility.approach(value, target, AnimationUtility.deltaTime(), speed), target, 0.01f); + } + + static float easeOutCubic(float value) { + float t = CombatProtocolHeuristicsEditorState.clamp(value, 0f, 1f); + float inv = 1f - t; + return 1f - inv * inv * inv; + } + + static String sourceName(CombatProtocolHeuristicSource source) { + return ClickGuiI18n.tr("setting.common.combat.protocol_heuristics.option." + source.key(), switch (source) { case MESSAGES -> "Messages"; case OVERLAY -> "Overlay"; case BOSSBAR -> "Boss bar"; }); + } + + static String defaultSourceHint(CombatProtocolHeuristicSource source) { + return switch (source) { case MESSAGES -> "Chat and system messages received from the server"; case OVERLAY -> "Action bar and overlay text"; case BOSSBAR -> "Visible boss bar titles"; }; + } + + static String tr(String suffix, String fallback, Object... args) { + return ClickGuiI18n.tr("clickgui.protocol_heuristics." + suffix, fallback, args); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/protocol/state/ProtocolInlineEditor.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/protocol/state/ProtocolInlineEditor.java new file mode 100644 index 0000000..6b045d8 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/protocol/state/ProtocolInlineEditor.java @@ -0,0 +1,45 @@ +package dev.loki.lovisual.features.gui.clickgui.protocol; + +import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.util.combat.protocol.heuristics.CombatProtocolHeuristicSource; +import dev.loki.lovisual.util.combat.protocol.heuristics.CombatProtocolHeuristicsConfig; +import dev.loki.lovisual.util.text.edit.TextSelection; +import org.lwjgl.glfw.GLFW; +import java.util.LinkedHashSet; +import java.util.Set; + +final class ProtocolInlineEditor { + static final float LINE_H = 8f * CombatProtocolHeuristicsEditorState.SCALE; + static final float FONT_SIZE = 5.9f * CombatProtocolHeuristicsEditorState.SCALE; + static final float GUTTER_W = 19f * CombatProtocolHeuristicsEditorState.SCALE; + static final float TEXT_PAD = 5f * CombatProtocolHeuristicsEditorState.SCALE; + final CombatProtocolHeuristicSource source; + final CombatProtocolHeuristicsConfig.ProtocolFamily family; + final StringBuilder buffer = new StringBuilder(); + final TextSelection selection = new TextSelection(); + CombatProtocolHeuristicsEditorState.Rect bounds = CombatProtocolHeuristicsEditorState.Rect.EMPTY; + CombatProtocolHeuristicsEditorState.Rect textArea = CombatProtocolHeuristicsEditorState.Rect.EMPTY; + int caret; + int preferredColumn = -1; + float scrollY, targetScrollY, scrollX, targetScrollX, focusAnim, hoverAnim, blink; + boolean focused, dragging; + ProtocolInlineEditor(CombatProtocolHeuristicSource source, CombatProtocolHeuristicsConfig.ProtocolFamily family, Set patterns) { this.source = source; this.family = family; replace(patterns); } + void replace(Set patterns) { buffer.setLength(0); if (patterns != null && !patterns.isEmpty()) buffer.append(String.join("\n", patterns)); caret = Math.min(caret, buffer.length()); selection.clear(); targetScrollX = targetScrollY = scrollX = scrollY = 0f; } + void setFocused(boolean v) { focused = v; blink = 0f; if (!v) { dragging = false; selection.clear(); } } + int patternCount() { return parsedPatterns().size(); } + void render(float x, float y, float w, float h, TextRenderer regular, TextRenderer medium, float mx, float my, dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette p, dev.loki.lovisual.features.theme.Themes.Theme t) { ProtocolInlineEditorRenderer.render(this, x, y, w, h, regular, medium, mx, my, p, t); } + boolean mousePressed(float mx, float my, int button) { if (button != GLFW.GLFW_MOUSE_BUTTON_LEFT || !textArea.contains(mx, my)) return false; int target = caretFromPoint(mx, my); boolean shift = CombatProtocolHeuristicsEditorState.shiftDown(); if (shift) { if (!selection.hasCaret()) selection.begin(0, caret); selection.updateCaret(0, target); } else { selection.clear(); selection.begin(0, target); } caret = target; preferredColumn = -1; dragging = true; blink = 0f; ensureCaretVisible(); return true; } + void mouseMoved(float mx, float my) { if (!focused || !dragging) return; int target = caretFromPoint(mx, my); if (!selection.hasCaret()) selection.begin(0, caret); selection.updateCaret(0, target); caret = target; preferredColumn = -1; blink = 0f; ensureCaretVisible(); } + void mouseReleased(int button) { if (button == GLFW.GLFW_MOUSE_BUTTON_LEFT) dragging = false; } + boolean scroll(float mx, float my, double delta) { if (!bounds.contains(mx, my)) return false; targetScrollY -= (float) delta * LINE_H * 2f; clampScroll(); return true; } + boolean charTyped(char chr) { if (!focused || Character.isISOControl(chr)) return false; ProtocolInlineEditorRenderer.insertText(this, String.valueOf(chr)); return true; } + boolean keyPressed(int key, int modifiers) { if (!focused) return false; boolean ctrl = (modifiers & GLFW.GLFW_MOD_CONTROL) != 0; boolean shift = (modifiers & GLFW.GLFW_MOD_SHIFT) != 0; if (ctrl) { switch (key) { case GLFW.GLFW_KEY_A -> ProtocolInlineEditorRenderer.selectAll(this); case GLFW.GLFW_KEY_C -> ProtocolInlineEditorRenderer.copy(this); case GLFW.GLFW_KEY_X -> ProtocolInlineEditorRenderer.cut(this); case GLFW.GLFW_KEY_V -> ProtocolInlineEditorRenderer.insertText(this, dev.loki.lovisual.util.text.edit.ClipboardUtil.get()); default -> { return true; } } return true; } switch (key) { case GLFW.GLFW_KEY_ENTER, GLFW.GLFW_KEY_KP_ENTER -> ProtocolInlineEditorRenderer.insertText(this, "\n"); case GLFW.GLFW_KEY_BACKSPACE -> ProtocolInlineEditorRenderer.backspace(this); case GLFW.GLFW_KEY_DELETE -> ProtocolInlineEditorRenderer.deleteForward(this); case GLFW.GLFW_KEY_LEFT -> ProtocolInlineEditorRenderer.moveHorizontal(this, -1, shift); case GLFW.GLFW_KEY_RIGHT -> ProtocolInlineEditorRenderer.moveHorizontal(this, 1, shift); case GLFW.GLFW_KEY_UP -> ProtocolInlineEditorRenderer.moveVertical(this, -1, shift); case GLFW.GLFW_KEY_DOWN -> ProtocolInlineEditorRenderer.moveVertical(this, 1, shift); case GLFW.GLFW_KEY_HOME -> ProtocolInlineEditorRenderer.moveLineBoundary(this, false, shift); case GLFW.GLFW_KEY_END -> ProtocolInlineEditorRenderer.moveLineBoundary(this, true, shift); default -> { return true; } } return true; } + Set parsedPatterns() { LinkedHashSet out = new LinkedHashSet<>(); for (String line : lines()) { String clean = line.trim(); if (!clean.isEmpty()) out.add(clean); } return out; } + int caretFromPoint(float mx, float my) { String[] lines = lines(); int line = CombatProtocolHeuristicsEditorState.clamp((int) Math.floor((my - textArea.y() - 3f * CombatProtocolHeuristicsEditorState.SCALE + scrollY) / LINE_H), 0, lines.length - 1); String text = lines[line]; float localX = mx - (textArea.x() + GUTTER_W + TEXT_PAD) + scrollX; int column = 0; TextRenderer font = ClickGuiRenderer.getInterRegular(); for (int i = 1; i <= text.length(); i++) { float prev = ClickGuiRenderer.textWidth(font, text.substring(0, i - 1), FONT_SIZE); float next = ClickGuiRenderer.textWidth(font, text.substring(0, i), FONT_SIZE); if (localX < (prev + next) * 0.5f) break; column = i; } return CombatProtocolHeuristicsEditorState.indexForLineColumn(lines, line, column); } + void ensureCaretVisible() { ProtocolInlineEditorRenderer.ensureVisible(this); } + void clampScroll() { ProtocolInlineEditorRenderer.clamp(this); } + String[] lines() { return buffer.toString().split("\n", -1); } + void changed() { CombatProtocolHeuristicsConfig.get().setPatterns(source, family, parsedPatterns()); clampScroll(); ensureCaretVisible(); } + boolean deleteSelection() { if (!selection.hasRange()) return false; int s = CombatProtocolHeuristicsEditorState.clamp(Math.min(selection.anchor(), selection.caret()), 0, buffer.length()); int e = CombatProtocolHeuristicsEditorState.clamp(Math.max(selection.anchor(), selection.caret()), 0, buffer.length()); if (e <= s) return false; buffer.delete(s, e); caret = s; selection.clear(); return true; } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/protocol/state/ProtocolInlineEditorRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/protocol/state/ProtocolInlineEditorRenderer.java new file mode 100644 index 0000000..6cdc81b --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/protocol/state/ProtocolInlineEditorRenderer.java @@ -0,0 +1,192 @@ +package dev.loki.lovisual.features.gui.clickgui.protocol; + +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; +import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; +import dev.loki.lovisual.features.theme.Themes; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.render.helpers.clip.ScissorFunction; +import dev.loki.lovisual.render.helpers.util.SystemCursor; +import dev.loki.lovisual.util.combat.protocol.heuristics.CombatProtocolHeuristicsConfig; + +import java.util.List; + +final class ProtocolInlineEditorRenderer { + private ProtocolInlineEditorRenderer() {} + + static void render(ProtocolInlineEditor e, float x, float y, float w, float h, TextRenderer regular, TextRenderer medium, float mx, float my, SettingsGuiPalette palette, Themes.Theme theme) { + e.bounds = new CombatProtocolHeuristicsEditorState.Rect(x, y, w, h); + boolean hovered = e.bounds.contains(mx, my); + e.focusAnim = CombatProtocolRenderer.animate(e.focusAnim, e.focused, 13f); + e.hoverAnim = CombatProtocolRenderer.animate(e.hoverAnim, hovered, 13f); + e.blink += AnimationUtility.deltaTime(); + e.scrollY = AnimationUtility.approach(e.scrollY, e.targetScrollY, 0.28f); + e.scrollX = AnimationUtility.approach(e.scrollX, e.targetScrollX, 0.28f); + int baseA = SettingsGuiPalette.mix(palette.moduleCardTop(), palette.panelBgLeft(), 0.30f); + int baseB = SettingsGuiPalette.mix(palette.moduleCardBottom(), palette.panelBgRight(), 0.28f); + int bgA = SettingsGuiPalette.mix(baseA, theme.accentSoft(), e.focusAnim * 0.14f + e.hoverAnim * 0.04f); + int bgB = SettingsGuiPalette.mix(baseB, theme.accent(), e.focusAnim * 0.08f + e.hoverAnim * 0.03f); + ClickGuiRenderer.drawRoundedRectGradient(x, y, w, h, 5.5f * CombatProtocolHeuristicsEditorState.SCALE, bgA, bgB, 85f); + ClickGuiRenderer.drawRoundedRectStrokeGradient(x, y, w, h, 5.5f * CombatProtocolHeuristicsEditorState.SCALE, (0.45f + e.focusAnim * 0.25f) * CombatProtocolHeuristicsEditorState.SCALE, SettingsGuiPalette.mix(palette.panelStroke(), theme.accentSoft(), 0.18f + e.focusAnim * 0.66f), SettingsGuiPalette.mix(palette.panelStroke(), theme.accent(), 0.10f + e.focusAnim * 0.54f), 0f); + if (e.focused) ClickGuiRenderer.drawRoundedRectGlow(x, y, w, h, 5.5f * CombatProtocolHeuristicsEditorState.SCALE, 6f * CombatProtocolHeuristicsEditorState.SCALE, SettingsGuiPalette.withAlpha(theme.accentSoft(), Math.round(25f + 25f * e.focusAnim))); + boolean legacy = e.family == CombatProtocolHeuristicsConfig.ProtocolFamily.LEGACY; + String name = legacy ? CombatProtocolRenderer.tr("legacy", "Legacy (1.8)") : CombatProtocolRenderer.tr("modern", "Modern (1.9+)"); + int invalid = invalidCount(List.of(e.lines())); + String meta = CombatProtocolRenderer.tr("pattern_count", "%s patterns", e.patternCount()); + if (invalid > 0) meta += " \u00b7 " + CombatProtocolRenderer.tr("invalid_count", "%s invalid", invalid); + ClickGuiRenderer.drawText(medium, name, x + 7f * CombatProtocolHeuristicsEditorState.SCALE, y + 5.5f * CombatProtocolHeuristicsEditorState.SCALE, 6.7f * CombatProtocolHeuristicsEditorState.SCALE, palette.moduleTitleText(), false); + float metaSize = 5.3f * CombatProtocolHeuristicsEditorState.SCALE; + float metaW = ClickGuiRenderer.textWidth(regular, meta, metaSize); + ClickGuiRenderer.drawText(regular, meta, x + w - 7f * CombatProtocolHeuristicsEditorState.SCALE - metaW, y + 7f * CombatProtocolHeuristicsEditorState.SCALE, metaSize, invalid > 0 ? 0xFFFF8A80 : palette.panelMuted(), false); + float areaY = y + 19f * CombatProtocolHeuristicsEditorState.SCALE; + e.textArea = new CombatProtocolHeuristicsEditorState.Rect(x + 5f * CombatProtocolHeuristicsEditorState.SCALE, areaY, w - 10f * CombatProtocolHeuristicsEditorState.SCALE, h - 24f * CombatProtocolHeuristicsEditorState.SCALE); + int areaBg = SettingsGuiPalette.withAlpha(SettingsGuiPalette.darken(palette.panelBgRight(), 0.18f), 190); + ClickGuiRenderer.drawRoundedRect(e.textArea.x(), e.textArea.y(), e.textArea.w(), e.textArea.h(), 3.5f * CombatProtocolHeuristicsEditorState.SCALE, areaBg); + ClickGuiRenderer.drawRoundedRectStroke(e.textArea.x(), e.textArea.y(), e.textArea.w(), e.textArea.h(), 3.5f * CombatProtocolHeuristicsEditorState.SCALE, 0.35f * CombatProtocolHeuristicsEditorState.SCALE, SettingsGuiPalette.mix(palette.panelStroke(), theme.accentSoft(), e.focusAnim * 0.42f)); + renderText(e, regular, palette, theme); + if (hovered) SystemCursor.set(SystemCursor.CursorType.TEXT); + } + + private static void renderText(ProtocolInlineEditor e, TextRenderer font, SettingsGuiPalette palette, Themes.Theme theme) { + String[] lines = e.lines(); + float viewX = e.textArea.x() + ProtocolInlineEditor.GUTTER_W + ProtocolInlineEditor.TEXT_PAD - e.scrollX; + float viewY = e.textArea.y() + 3f * CombatProtocolHeuristicsEditorState.SCALE - e.scrollY; + boolean clipped = ScissorFunction.pushRaw(e.textArea.x(), e.textArea.y(), e.textArea.w(), e.textArea.h()); + int globalOffset = 0; + int caretLine = 0; + int caretCol = 0; + for (int i = 0; i < lines.length; i++) { + String line = lines[i]; + float lineY = viewY + i * ProtocolInlineEditor.LINE_H; + int lineStart = globalOffset; + int lineEnd = lineStart + line.length(); + if (e.caret >= lineStart && e.caret <= lineEnd) { caretLine = i; caretCol = e.caret - lineStart; } + if (lineY + ProtocolInlineEditor.LINE_H >= e.textArea.y() && lineY <= e.textArea.y() + e.textArea.h()) { + if (e.focused && e.selection.hasRange() && e.selection.appliesToLine(0)) { + int selStart = Math.min(e.selection.anchor(), e.selection.caret()); + int selEnd = Math.max(e.selection.anchor(), e.selection.caret()); + int a = Math.max(lineStart, selStart); + int b = Math.min(lineEnd, selEnd); + if (b > a) { + float x0 = viewX + ClickGuiRenderer.textWidth(font, line.substring(0, a - lineStart), ProtocolInlineEditor.FONT_SIZE); + float x1 = viewX + ClickGuiRenderer.textWidth(font, line.substring(0, b - lineStart), ProtocolInlineEditor.FONT_SIZE); + ClickGuiRenderer.drawRoundedRect(x0, lineY, Math.max(1f, x1 - x0), ProtocolInlineEditor.LINE_H, 2f * CombatProtocolHeuristicsEditorState.SCALE, SettingsGuiPalette.withAlpha(theme.accentSoft(), 155)); + } + } + String lineNumber = Integer.toString(i + 1); + float numberW = ClickGuiRenderer.textWidth(font, lineNumber, 5.1f * CombatProtocolHeuristicsEditorState.SCALE); + ClickGuiRenderer.drawText(font, lineNumber, e.textArea.x() + ProtocolInlineEditor.GUTTER_W - 4f * CombatProtocolHeuristicsEditorState.SCALE - numberW, lineY + 0.5f * CombatProtocolHeuristicsEditorState.SCALE, 5.1f * CombatProtocolHeuristicsEditorState.SCALE, palette.panelMuted(), false); + int textColor = line.isBlank() || CombatProtocolHeuristicsEditorState.validPattern(line) ? palette.moduleDescriptionText() : 0xFFFF8A80; + ClickGuiRenderer.drawText(font, line, viewX, lineY + 0.2f * CombatProtocolHeuristicsEditorState.SCALE, ProtocolInlineEditor.FONT_SIZE, textColor, false); + } + globalOffset = lineEnd + 1; + } + ClickGuiRenderer.drawRect(e.textArea.x() + ProtocolInlineEditor.GUTTER_W, e.textArea.y() + 2f * CombatProtocolHeuristicsEditorState.SCALE, 0.4f * CombatProtocolHeuristicsEditorState.SCALE, e.textArea.h() - 4f * CombatProtocolHeuristicsEditorState.SCALE, palette.panelDivider()); + if (e.focused && ((int) (e.blink * 2f) & 1) == 0) { + String line = lines[Math.max(0, Math.min(caretLine, lines.length - 1))]; + float caretX = viewX + ClickGuiRenderer.textWidth(font, line.substring(0, Math.min(caretCol, line.length())), ProtocolInlineEditor.FONT_SIZE); + float caretY = viewY + caretLine * ProtocolInlineEditor.LINE_H; + ClickGuiRenderer.drawRoundedRect(caretX, caretY, 0.65f * CombatProtocolHeuristicsEditorState.SCALE, ProtocolInlineEditor.LINE_H, 0.3f * CombatProtocolHeuristicsEditorState.SCALE, theme.accent()); + } + if (clipped) ScissorFunction.pop(); + } + + private static int invalidCount(Iterable patterns) { + int invalid = 0; + if (patterns == null) return invalid; + for (String pattern : patterns) if (pattern != null && !pattern.isBlank() && !CombatProtocolHeuristicsEditorState.validPattern(pattern)) invalid++; + return invalid; + } + + static void ensureVisible(ProtocolInlineEditor e) { + String[] lines = e.lines(); + CombatProtocolHeuristicsEditorState.LineColumn pos = CombatProtocolHeuristicsEditorState.lineColumn(e.caret, lines); + float caretY = pos.line() * ProtocolInlineEditor.LINE_H; + float visibleH = Math.max(ProtocolInlineEditor.LINE_H, e.textArea.h() - 6f * CombatProtocolHeuristicsEditorState.SCALE); + if (caretY < e.targetScrollY) e.targetScrollY = caretY; + if (caretY + ProtocolInlineEditor.LINE_H > e.targetScrollY + visibleH) e.targetScrollY = caretY + ProtocolInlineEditor.LINE_H - visibleH; + dev.loki.lovisual.render.engine.text.render.TextRenderer font = ClickGuiRenderer.getInterRegular(); + String line = lines[pos.line()]; + float caretX = ClickGuiRenderer.textWidth(font, line.substring(0, Math.min(pos.column(), line.length())), ProtocolInlineEditor.FONT_SIZE); + float visibleW = Math.max(20f * CombatProtocolHeuristicsEditorState.SCALE, e.textArea.w() - ProtocolInlineEditor.GUTTER_W - ProtocolInlineEditor.TEXT_PAD * 2f); + if (caretX < e.targetScrollX) e.targetScrollX = caretX; + if (caretX + 2f * CombatProtocolHeuristicsEditorState.SCALE > e.targetScrollX + visibleW) e.targetScrollX = caretX + 2f * CombatProtocolHeuristicsEditorState.SCALE - visibleW; + clamp(e); + } + + static void clamp(ProtocolInlineEditor e) { + String[] lines = e.lines(); + float maxY = Math.max(0f, lines.length * ProtocolInlineEditor.LINE_H - Math.max(ProtocolInlineEditor.LINE_H, e.textArea.h() - 6f * CombatProtocolHeuristicsEditorState.SCALE)); + float maxLineW = 0f; + dev.loki.lovisual.render.engine.text.render.TextRenderer font = ClickGuiRenderer.getInterRegular(); + for (String l : lines) maxLineW = Math.max(maxLineW, ClickGuiRenderer.textWidth(font, l, ProtocolInlineEditor.FONT_SIZE)); + float maxX = Math.max(0f, maxLineW - Math.max(20f * CombatProtocolHeuristicsEditorState.SCALE, e.textArea.w() - ProtocolInlineEditor.GUTTER_W - ProtocolInlineEditor.TEXT_PAD * 2f)); + e.targetScrollY = CombatProtocolHeuristicsEditorState.clamp(e.targetScrollY, 0f, maxY); + e.targetScrollX = CombatProtocolHeuristicsEditorState.clamp(e.targetScrollX, 0f, maxX); + } + + static void insertText(ProtocolInlineEditor e, String text) { + if (text == null || text.isEmpty()) return; + e.deleteSelection(); + String n = text.replace("\r\n", "\n").replace('\r', '\n'); + e.buffer.insert(e.caret, n); + e.caret += n.length(); + e.selection.clear(); + e.preferredColumn = -1; + e.blink = 0f; + e.changed(); + } + + static void backspace(ProtocolInlineEditor e) { + if (e.deleteSelection()) { e.changed(); return; } + if (e.caret <= 0) return; + e.buffer.deleteCharAt(e.caret - 1); + e.caret--; + e.selection.clear(); + e.preferredColumn = -1; + e.changed(); + } + + static void deleteForward(ProtocolInlineEditor e) { + if (e.deleteSelection()) { e.changed(); return; } + if (e.caret >= e.buffer.length()) return; + e.buffer.deleteCharAt(e.caret); + e.selection.clear(); + e.preferredColumn = -1; + e.changed(); + } + + static void moveHorizontal(ProtocolInlineEditor e, int delta, boolean extend) { + int target; + if (!extend && e.selection.hasRange()) target = delta < 0 ? Math.min(e.selection.anchor(), e.selection.caret()) : Math.max(e.selection.anchor(), e.selection.caret()); + else target = CombatProtocolHeuristicsEditorState.clamp(e.caret + delta, 0, e.buffer.length()); + updateSelection(e, target, extend); + e.caret = target; e.preferredColumn = -1; e.blink = 0f; e.ensureCaretVisible(); + } + + static void moveVertical(ProtocolInlineEditor e, int delta, boolean extend) { + String[] lines = e.lines(); CombatProtocolHeuristicsEditorState.LineColumn cur = CombatProtocolHeuristicsEditorState.lineColumn(e.caret, lines); + if (e.preferredColumn < 0) e.preferredColumn = cur.column(); + int tl = CombatProtocolHeuristicsEditorState.clamp(cur.line() + delta, 0, lines.length - 1); + int target = CombatProtocolHeuristicsEditorState.indexForLineColumn(lines, tl, Math.min(e.preferredColumn, lines[tl].length())); + updateSelection(e, target, extend); e.caret = target; e.blink = 0f; e.ensureCaretVisible(); + } + + static void moveLineBoundary(ProtocolInlineEditor e, boolean end, boolean extend) { + String[] lines = e.lines(); CombatProtocolHeuristicsEditorState.LineColumn cur = CombatProtocolHeuristicsEditorState.lineColumn(e.caret, lines); + int target = CombatProtocolHeuristicsEditorState.indexForLineColumn(lines, cur.line(), end ? lines[cur.line()].length() : 0); + updateSelection(e, target, extend); e.caret = target; e.preferredColumn = -1; e.blink = 0f; e.ensureCaretVisible(); + } + + static void updateSelection(ProtocolInlineEditor e, int target, boolean extend) { + if (extend) { if (!e.selection.hasCaret()) e.selection.begin(0, e.caret); e.selection.updateCaret(0, target); } else e.selection.clear(); + } + + static void selectAll(ProtocolInlineEditor e) { e.selection.begin(0, 0); e.selection.updateCaret(0, e.buffer.length()); e.caret = e.buffer.length(); e.preferredColumn = -1; e.blink = 0f; e.ensureCaretVisible(); } + + static void copy(ProtocolInlineEditor e) { + if (e.selection.hasRange()) { int s = CombatProtocolHeuristicsEditorState.clamp(Math.min(e.selection.anchor(), e.selection.caret()), 0, e.buffer.length()); int en = CombatProtocolHeuristicsEditorState.clamp(Math.max(e.selection.anchor(), e.selection.caret()), 0, e.buffer.length()); if (en > s) dev.loki.lovisual.util.text.edit.ClipboardUtil.copy(e.buffer.substring(s, en)); } else if (!e.buffer.isEmpty()) dev.loki.lovisual.util.text.edit.ClipboardUtil.copy(e.buffer.toString()); + } + + static void cut(ProtocolInlineEditor e) { if (!e.selection.hasRange()) return; copy(e); if (e.deleteSelection()) e.changed(); } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/render/ClickGuiDrawHelpers.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/render/ClickGuiDrawHelpers.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/render/ClickGuiDrawHelpers.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/render/ClickGuiDrawHelpers.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/render/ClickGuiRenderEngine.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/render/ClickGuiRenderEngine.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/render/ClickGuiRenderEngine.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/render/ClickGuiRenderEngine.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/render/ClickGuiRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/render/ClickGuiRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/render/ClickGuiRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/render/ClickGuiRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/render/ClickGuiTextBatch.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/render/ClickGuiTextBatch.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/render/ClickGuiTextBatch.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/render/ClickGuiTextBatch.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/screen/ClickGuiLifecycle.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/screen/ClickGuiLifecycle.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/screen/ClickGuiLifecycle.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/screen/ClickGuiLifecycle.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/screen/ClickGuiScreen.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/screen/ClickGuiScreen.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/screen/ClickGuiScreen.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/screen/ClickGuiScreen.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/screen/ClickGuiScreenManager.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/screen/ClickGuiScreenManager.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/screen/ClickGuiScreenManager.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/screen/ClickGuiScreenManager.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/screen/ClickGuiTopLayer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/screen/ClickGuiTopLayer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/screen/ClickGuiTopLayer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/screen/ClickGuiTopLayer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/sections/ClickGuiSection.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/sections/ClickGuiSection.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/sections/ClickGuiSection.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/sections/ClickGuiSection.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/sections/ModulesSection.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/sections/ModulesSection.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/sections/ModulesSection.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/sections/ModulesSection.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/sections/SettingsSection.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/sections/SettingsSection.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/sections/SettingsSection.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/sections/SettingsSection.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/sections/settings/SettingsTabRuntime.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/sections/settings/SettingsTabRuntime.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/sections/settings/SettingsTabRuntime.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/sections/settings/SettingsTabRuntime.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bind/FunctionBindSetting.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bind/FunctionBindSetting.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bind/FunctionBindSetting.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bind/FunctionBindSetting.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bind/FunctionBindSettingRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bind/FunctionBindSettingRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bind/FunctionBindSettingRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bind/FunctionBindSettingRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bind/KeyBindSetting.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bind/KeyBindSetting.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bind/KeyBindSetting.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bind/KeyBindSetting.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bind/KeyBindSettingRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bind/KeyBindSettingRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bind/KeyBindSettingRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bind/KeyBindSettingRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bool/BooleanSetting.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bool/BooleanSetting.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bool/BooleanSetting.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bool/BooleanSetting.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bool/BooleanSettingRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bool/BooleanSettingRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bool/BooleanSettingRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/bool/BooleanSettingRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/ColorSetting.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/ColorSetting.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/ColorSetting.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/ColorSetting.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/ColorSettingMath.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/ColorSettingMath.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/ColorSettingMath.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/ColorSettingMath.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/ColorSettingNoAlpha.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/ColorSettingNoAlpha.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/ColorSettingNoAlpha.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/ColorSettingNoAlpha.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/ConfigColorSettingNoAlpha.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/ConfigColorSettingNoAlpha.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/ConfigColorSettingNoAlpha.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/ConfigColorSettingNoAlpha.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/hex/ColorSettingHexInput.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/hex/ColorSettingHexInput.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/hex/ColorSettingHexInput.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/hex/ColorSettingHexInput.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/hex/ColorSettingHexState.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/hex/ColorSettingHexState.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/hex/ColorSettingHexState.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/hex/ColorSettingHexState.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/preset/ColorPresetStore.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/preset/ColorPresetStore.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/preset/ColorPresetStore.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/preset/ColorPresetStore.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/preset/ColorSettingPresetHandler.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/preset/ColorSettingPresetHandler.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/preset/ColorSettingPresetHandler.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/preset/ColorSettingPresetHandler.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/preset/ColorSettingPresetRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/preset/ColorSettingPresetRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/preset/ColorSettingPresetRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/preset/ColorSettingPresetRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/render/ColorSettingDrag.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/render/ColorSettingDrag.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/render/ColorSettingDrag.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/render/ColorSettingDrag.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/render/ColorSettingLayout.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/render/ColorSettingLayout.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/render/ColorSettingLayout.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/render/ColorSettingLayout.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/render/ColorSettingRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/render/ColorSettingRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/render/ColorSettingRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/render/ColorSettingRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/render/ColorSettingSquareRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/render/ColorSettingSquareRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/render/ColorSettingSquareRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/color/render/ColorSettingSquareRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/cooldown/CooldownRulesSetting.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/cooldown/CooldownRulesSetting.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/cooldown/CooldownRulesSetting.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/cooldown/CooldownRulesSetting.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/cooldown/ProtocolHeuristicsSetting.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/cooldown/ProtocolHeuristicsSetting.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/cooldown/ProtocolHeuristicsSetting.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/cooldown/ProtocolHeuristicsSetting.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/core/Setting.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/core/Setting.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/core/Setting.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/core/Setting.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/core/SettingFactory.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/core/SettingFactory.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/core/SettingFactory.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/core/SettingFactory.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/group/GroupSetting.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/group/GroupSetting.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/group/GroupSetting.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/group/GroupSetting.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/group/GroupSettingRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/group/GroupSettingRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/group/GroupSettingRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/group/GroupSettingRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/i18n/I18nDuplicateScanner.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/i18n/I18nDuplicateScanner.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/i18n/I18nDuplicateScanner.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/i18n/I18nDuplicateScanner.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/i18n/I18nPreflightContributors.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/i18n/I18nPreflightContributors.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/i18n/I18nPreflightContributors.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/i18n/I18nPreflightContributors.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/i18n/I18nPreflightManager.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/i18n/I18nPreflightManager.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/i18n/I18nPreflightManager.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/i18n/I18nPreflightManager.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/mode/ModeSetting.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/mode/ModeSetting.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/mode/ModeSetting.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/mode/ModeSetting.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/mode/ModeSettingRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/mode/ModeSettingRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/mode/ModeSettingRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/mode/ModeSettingRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/owner/PickerDetailOwner.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/owner/PickerDetailOwner.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/owner/PickerDetailOwner.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/owner/PickerDetailOwner.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/owner/TextEditorOwner.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/owner/TextEditorOwner.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/owner/TextEditorOwner.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/owner/TextEditorOwner.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/render/SettingRenderContext.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/render/SettingRenderContext.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/render/SettingRenderContext.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/render/SettingRenderContext.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/render/SettingRenderSurface.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/render/SettingRenderSurface.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/render/SettingRenderSurface.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/render/SettingRenderSurface.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/render/SettingRendererBridge.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/render/SettingRendererBridge.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/render/SettingRendererBridge.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/render/SettingRendererBridge.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/render/UnifiedSettingRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/render/UnifiedSettingRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/render/UnifiedSettingRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/render/UnifiedSettingRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/skin/ModulesSettingsSkin.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/skin/ModulesSettingsSkin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/skin/ModulesSettingsSkin.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/skin/ModulesSettingsSkin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/skin/SettingSkinUtil.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/skin/SettingSkinUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/skin/SettingSkinUtil.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/skin/SettingSkinUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/skin/UnifiedSettingsSkin.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/skin/UnifiedSettingsSkin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/skin/UnifiedSettingsSkin.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/skin/UnifiedSettingsSkin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/slider/SliderSetting.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/slider/SliderSetting.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/slider/SliderSetting.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/slider/SliderSetting.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/slider/SliderSettingRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/slider/SliderSettingRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/slider/SliderSettingRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/slider/SliderSettingRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/slider/SliderTextInputHandler.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/slider/SliderTextInputHandler.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/slider/SliderTextInputHandler.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/slider/SliderTextInputHandler.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/slider/SliderTextInputState.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/slider/SliderTextInputState.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/slider/SliderTextInputState.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/slider/SliderTextInputState.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/TextSetting.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/TextSetting.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/TextSetting.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/TextSetting.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/TextSettingRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/TextSettingRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/TextSettingRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/TextSettingRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/TextSettingTextInputHandler.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/TextSettingTextInputHandler.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/TextSettingTextInputHandler.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/TextSettingTextInputHandler.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/list/TextListSetting.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/list/TextListSetting.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/list/TextListSetting.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/list/TextListSetting.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/list/TextListSettingRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/list/TextListSettingRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/list/TextListSettingRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/list/TextListSettingRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/list/TextListTextEditHandler.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/list/TextListTextEditHandler.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/list/TextListTextEditHandler.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/text/list/TextListTextEditHandler.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/util/BindRendererUtil.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/util/BindRendererUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/util/BindRendererUtil.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/util/BindRendererUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/util/SettingTextInputUtil.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/util/SettingTextInputUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/util/SettingTextInputUtil.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/util/SettingTextInputUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/tabs/ClickGuiSearch.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/tabs/ClickGuiSearch.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/tabs/ClickGuiSearch.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/tabs/ClickGuiSearch.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/tabs/ClickGuiTabBar.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/tabs/ClickGuiTabBar.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/tabs/ClickGuiTabBar.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/tabs/ClickGuiTabBar.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/tabs/ClickGuiTabState.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/tabs/ClickGuiTabState.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/tabs/ClickGuiTabState.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/tabs/ClickGuiTabState.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/ClickGuiI18n.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/ClickGuiI18n.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/ClickGuiI18n.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/ClickGuiI18n.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/ClickGuiMath.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/ClickGuiMath.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/ClickGuiMath.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/ClickGuiMath.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/ClickGuiRichTextRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/ClickGuiRichTextRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/ClickGuiRichTextRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/ClickGuiRichTextRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/tooltip/ClickGuiHintOverlay.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/tooltip/ClickGuiHintOverlay.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/tooltip/ClickGuiHintOverlay.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/tooltip/ClickGuiHintOverlay.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/tooltip/SettingTooltip.java b/mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/tooltip/SettingTooltip.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/tooltip/SettingTooltip.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/tooltip/SettingTooltip.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/anchor/HudAnchorX.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/anchor/HudAnchorX.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/anchor/HudAnchorX.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/anchor/HudAnchorX.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/anchor/HudAnchorY.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/anchor/HudAnchorY.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/anchor/HudAnchorY.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/anchor/HudAnchorY.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/anchor/HudRenderSpace.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/anchor/HudRenderSpace.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/anchor/HudRenderSpace.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/anchor/HudRenderSpace.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/anchor/WidgetAnchorSide.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/anchor/WidgetAnchorSide.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/anchor/WidgetAnchorSide.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/anchor/WidgetAnchorSide.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/base/AbstractHudElement.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/base/AbstractHudElement.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/base/AbstractHudElement.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/base/AbstractHudElement.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/base/BaseHudElement.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/base/BaseHudElement.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/base/BaseHudElement.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/base/BaseHudElement.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/base/CooldownRender.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/base/CooldownRender.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/base/CooldownRender.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/base/CooldownRender.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/base/HudTextEffects.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/base/HudTextEffects.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/base/HudTextEffects.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/base/HudTextEffects.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/config/HudGlobalConfig.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/config/HudGlobalConfig.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/config/HudGlobalConfig.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/config/HudGlobalConfig.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/config/HudRenderUtil.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/config/HudRenderUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/config/HudRenderUtil.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/config/HudRenderUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/DraggableHudElement.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/DraggableHudElement.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/DraggableHudElement.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/DraggableHudElement.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/DraggableHudElementRegistry.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/DraggableHudElementRegistry.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/DraggableHudElementRegistry.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/DraggableHudElementRegistry.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/DraggableLayoutValue.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/DraggableLayoutValue.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/DraggableLayoutValue.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/DraggableLayoutValue.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/BetterChat.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/BetterChat.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/BetterChat.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/BetterChat.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/HudPanelLayoutModes.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/HudPanelLayoutModes.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/HudPanelLayoutModes.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/HudPanelLayoutModes.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/TimerIndicator.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/TimerIndicator.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/TimerIndicator.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/TimerIndicator.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/Triangulator.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/Triangulator.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/Triangulator.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/Triangulator.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/CombatLogHud.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/CombatLogHud.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/CombatLogHud.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/CombatLogHud.java diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/Inventory.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/Inventory.java new file mode 100644 index 0000000..d6fef3d --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/Inventory.java @@ -0,0 +1,153 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.combat; + +import dev.loki.lovisual.config.definition.SettingDef; +import dev.loki.lovisual.config.values.color.KeyBindValue; +import dev.loki.lovisual.config.values.color.RGBAColorValue; +import dev.loki.lovisual.config.values.color.RGBColorValue; +import dev.loki.lovisual.config.values.mode.BindMode; +import dev.loki.lovisual.config.values.mode.ModeValue; +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; +import dev.loki.lovisual.features.gui.hud.draggable.impl.HudPanelLayoutModes; +import dev.loki.lovisual.features.gui.hud.draggable.impl.scripted.ScriptedInventoryHudPanel; +import dev.loki.lovisual.features.gui.hud.registry.HudElementRegister; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.world.item.ItemStack; + +import java.util.ArrayList; +import java.util.List; + +@HudElementRegister(order = 220) +public final class Inventory extends DraggableHudElement { + + { + defaultLayout(1255.352f, 658.36005f); + } + + final HudGlobalConfig hud = HudGlobalConfig.get(); + final ScriptedInventoryHudPanel scriptedPanel = new ScriptedInventoryHudPanel(); + + final KeyBindValue toggleBind = new KeyBindValue("toggle_bind", "I"); + final NumberValue scaleValue = new NumberValue<>("inventory_scale", 1.32, HudPanelLayoutModes.SCALE_MIN, HudPanelLayoutModes.SCALE_MAX); + final ModeValue layoutMode = new ModeValue("inventory_layout", "Unified Divider", HudPanelLayoutModes.SPLIT_HEADER, HudPanelLayoutModes.UNIFIED_DIVIDER); + final ModeValue colorMode = new ModeValue("inventory_color_mode", "Theme", InventoryConstants.COLOR_THEME, InventoryConstants.COLOR_CUSTOM); + final ModeValue bgEffect = new ModeValue("inventory_bg_effect", "Blur", InventoryConstants.EFFECT_NONE, InventoryConstants.EFFECT_BLUR); + final RGBAColorValue bg = new RGBAColorValue("inventory_bg", "#F7343434"); + final RGBAColorValue bg2 = new RGBAColorValue("inventory_bg_secondary", "#F7161616"); + final NumberValue bgAlpha = new NumberValue<>("inventory_bg_alpha", 194, 0, 255); + final RGBColorValue stroke = new RGBColorValue("inventory_stroke", "#5A5A5A"); + final RGBColorValue text = new RGBColorValue("inventory_text", "#FFFFFF"); + final RGBColorValue muted = new RGBColorValue("inventory_muted", "#A5A5A5"); + final NumberValue blurAlpha = new NumberValue<>("inventory_blur_alpha", 255, 0, 255); + final BooleanValue headerIconPulse = new BooleanValue("inventory_header_icon_pulse", true); + final NumberValue headerIconPulseSpeed = new NumberValue<>("inventory_header_icon_pulse_speed", 15, 1, 60); + final NumberValue headerIconPulseIntensity = new NumberValue<>("inventory_header_icon_pulse_intensity", 100, 0, 100); + + final List pendingItemTasks = new ArrayList<>(InventoryConstants.COLS * InventoryConstants.ROWS); + + float displayWidth = -1.0f; + float displayHeight = -1.0f; + float visibilityAnim = 0.0f; + int uiHeaderLeft; + int uiHeaderRight; + int uiBodyLeft; + int uiBodyRight; + int uiOutline; + int uiText; + int uiMuted; + int uiCounter; + int uiTitleText; + int uiDivider; + int uiGridDivider; + int uiBlurTint; + boolean layoutReady; + boolean hitboxActive; + boolean displayVisible = true; + boolean toggleArmed = true; + boolean toggleInit = false; + + public Inventory() { + super("inventory", "Inventory", true); + } + + @Override + protected void defineSettings(List defs) { + defs.add(SettingDef.bind(toggleBind, BindMode.PRESS)); + defs.add(SettingDef.number(scaleValue)); + defs.add(SettingDef.mode(layoutMode)); + defs.add(SettingDef.mode(colorMode)); + defs.add(SettingDef.color(bg).visibleWhen(this::isCustomMode)); + defs.add(SettingDef.color(bg2).visibleWhen(this::isCustomMode)); + defs.add(SettingDef.colorNoAlpha(stroke).visibleWhen(this::isCustomMode)); + defs.add(SettingDef.number(bgAlpha).visibleWhen(this::isThemeMode)); + defs.add(SettingDef.colorNoAlpha(text).visibleWhen(this::isCustomMode)); + defs.add(SettingDef.colorNoAlpha(muted).visibleWhen(this::isCustomMode)); + defs.add(SettingDef.mode(bgEffect)); + defs.add(SettingDef.number(blurAlpha).visibleWhen(this::hasEffect)); + defs.add(SettingDef.bool(headerIconPulse)); + defs.add(SettingDef.number(headerIconPulseSpeed).visibleWhen(headerIconPulse::get)); + defs.add(SettingDef.number(headerIconPulseIntensity).visibleWhen(headerIconPulse::get)); + } + + @Override + public void applyDefaultPosition(int screenW, int screenH) { + this.x = 385.0f; + this.y = 40.0f; + } + + @Override + public boolean usesEngineRenderer() { + return true; + } + + @Override + public void onTick() { + InventoryRenderHelper.handleTick(this); + } + + @Override + public void renderEngine(Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta, int screenW, int screenH) { + InventoryRenderHelper.render(this, renderer, textRenderer, ctx, tickDelta, screenW, screenH); + } + + @Override + public void renderEngineForeground(Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta, int screenW, int screenH) { + InventoryRenderHelper.renderForeground(this, renderer, textRenderer, ctx, tickDelta, screenW, screenH); + } + + @Override + public boolean contains(float mx, float my) { + if (!hitboxActive && !DraggableHudElementRegistry.isForceVisible()) return false; + return super.contains(mx, my); + } + + boolean isThemeMode() { + return InventoryConstants.COLOR_THEME.equals(colorMode.get()); + } + + boolean isCustomMode() { + return InventoryConstants.COLOR_CUSTOM.equals(colorMode.get()); + } + + boolean hasEffect() { + return !InventoryConstants.EFFECT_NONE.equals(bgEffect.get()); + } + + void setSize(float w, float h) { + setBounds(getX(), getY(), w, h); + } + + record QueuedItemIcon(ItemStack stack, float x, float y, float scale, int seed) {} +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/Itemizer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/Itemizer.java new file mode 100644 index 0000000..6117953 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/Itemizer.java @@ -0,0 +1,193 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.combat; + +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.config.values.mode.EnumValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import dev.loki.lovisual.features.gui.hud.anchor.HudAnchorX; +import dev.loki.lovisual.features.gui.hud.anchor.HudAnchorY; +import dev.loki.lovisual.features.gui.hud.registry.HudElementRegister; +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; +import dev.loki.lovisual.features.gui.hud.script.HudScriptLayouts; +import dev.loki.lovisual.features.module.phase.HudPhase; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.runtime.UiRuntime; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.render.context.UiProjectionMode; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.render.context.UiRenderContext; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.runtime.CachedUiScriptRuntime; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.module.UiScriptModule; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.world.item.ItemStack; + +import java.util.LinkedHashMap; +import java.util.List; + +@HudElementRegister(order = 200) +public final class Itemizer extends DraggableHudElement { + static final String LAYOUT_ID = "lovisual:api/hud/draggable/itemizer"; + static final float BASE_ICON_CARD = 24.0f; + static final float BASE_COMPACT_W = 72.0f; + static final float BASE_COMPACT_H = 24.0f; + static final float BASE_GAP = 4.0f; + static final float DEFAULT_DURATION = 1.15f; + static final int MAX_INTERNAL_ENTRIES = 8; + + public static Itemizer INSTANCE; + + final CachedUiScriptRuntime runtime = new CachedUiScriptRuntime(HudScriptLayouts.runtimeReporter()); + final LinkedHashMap entries = new LinkedHashMap<>(); + final List itemTasks = new java.util.ArrayList<>(5); + final dev.loki.lovisual.render.engine.renderer.ui.runtime.script.module.UiScriptModuleHandle moduleHandle = HudScriptLayouts.handle(LAYOUT_ID); + + final NumberValue scale = num("itemizer_scale", "scale", 1.0, 0.5, 3.0); + final EnumValue displayMode = enumSetting("itemizer_display_mode", "display_mode", ItemizerDisplayMode.ICONS, ItemizerDisplayMode.values()); + final EnumValue direction = enumSetting("itemizer_direction", "direction", ItemizerDirection.HORIZONTAL, ItemizerDirection.values()); + final NumberValue duration = num("itemizer_duration", "duration", (double) DEFAULT_DURATION, 0.25, 3.0); + final NumberValue maxEntries = num("itemizer_max_entries", "max_entries", 3, 1, 5); + final BooleanValue showAutoEat = bool("itemizer_auto_eat", "auto_eat", true); + final BooleanValue showAutoTotem = bool("itemizer_auto_totem", "auto_totem", true); + final BooleanValue showElytraSwap = bool("itemizer_elytra_swap", "elytra_swap", true); + final BooleanValue blur = bool("itemizer_blur", "blur", true); + final NumberValue bgAlpha = num("itemizer_bg_alpha", "bg_alpha", 168, 0, 255); + + float visibilityAnim; + float displayWidth = -1.0f; + float displayHeight = -1.0f; + boolean foregroundReady; + + public Itemizer() { + super("itemizer", "Itemizer", true); + INSTANCE = this; + defaultLayout(-16.0f, 48.0f, "CENTER", "CENTER"); + } + + public static void showAutoEat(ItemStack stack) { show(ItemizerEvent.AUTO_EAT, stack, "Eat"); } + public static void showAutoTotem(ItemStack stack) { show(ItemizerEvent.AUTO_TOTEM, stack, "Totem"); } + public static void showElytraSwap(ItemStack stack) { show(ItemizerEvent.ELYTRA_SWAP, stack, "Swap"); } + public static void hideElytraSwap() { hide(ItemizerEvent.ELYTRA_SWAP); } + + private static void show(ItemizerEvent event, ItemStack stack, String label) { + Itemizer itemizer = INSTANCE; + if (itemizer == null || event == null || stack == null || stack.isEmpty()) return; + itemizer.push(event, stack, label); + } + + private static void hide(ItemizerEvent event) { + Itemizer itemizer = INSTANCE; + if (itemizer == null || event == null) return; + itemizer.entries.remove(event); + } + + @Override + public void applyDefaultPosition(int screenW, int screenH) { + float w = ItemizerModel.baseEntryWidth(this) * scale.get().floatValue(); + float h = ItemizerModel.baseEntryHeight(this) * scale.get().floatValue(); + this.x = (screenW - w) * 0.5f; + this.y = screenH * 0.5f + 48.0f; + setAnchors(HudAnchorX.FREE, HudAnchorY.FREE, this.x, this.y); + } + + @Override public boolean usesEngineRenderer() { return true; } + @Override public HudPhase getHudPhase() { return HudPhase.LAST; } + @Override public int getRenderOrder() { return 82; } + + @Override + public void renderEngine(Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta, int screenW, int screenH) { + foregroundReady = false; + itemTasks.clear(); + boolean forceVisible = DraggableHudElementRegistry.isForceVisible(); + if (!isEnabled() && !forceVisible) { width = 0.0f; height = 0.0f; return; } + float dt = AnimationUtility.deltaTime(); + List visible = forceVisible && entries.isEmpty() ? ItemizerModel.previewEntries(this) : ItemizerModel.updateEntries(this, dt); + boolean showWidget = !visible.isEmpty(); + visibilityAnim = HudRenderUtil.animateVisibility(visibilityAnim, showWidget); + if (!showWidget && visibilityAnim <= 0.0f) { width = 0.0f; height = 0.0f; return; } + float baseScale = scale.get().floatValue(); + int count = Math.max(1, visible.size()); + float entryW = ItemizerModel.baseEntryWidth(this) * baseScale; + float entryH = ItemizerModel.baseEntryHeight(this) * baseScale; + float gap = BASE_GAP * baseScale; + float targetWidth = direction.get() == ItemizerDirection.HORIZONTAL ? entryW * count + gap * Math.max(0, count - 1) : entryW; + float targetHeight = direction.get() == ItemizerDirection.HORIZONTAL ? entryH : entryH * count + gap * Math.max(0, count - 1); + displayWidth = HudRenderUtil.animateDimension(displayWidth, targetWidth); + displayHeight = HudRenderUtil.animateDimension(displayHeight, targetHeight); + width = displayWidth; height = displayHeight; + float drawScale = HudRenderUtil.visibilityScale(visibilityAnim); + float drawWidth = displayWidth * drawScale; + float drawHeight = displayHeight * drawScale; + if (drawWidth <= 0.0f || drawHeight <= 0.0f) return; + float drawX = x + (displayWidth - drawWidth) * 0.5f; + float drawY = y + (displayHeight - drawHeight) * 0.5f; + float drawBaseScale = baseScale * drawScale; + ItemizerRenderer.drawCards(this, renderer, visible, drawX, drawY, drawBaseScale, AnimationUtility.clamp01(visibilityAnim)); + UiScriptModule module = ItemizerProps.ensureModule(this); + if (module == null) return; + TextRenderer fallback = textRenderer != null ? textRenderer : TextRenderer.get(); + LinkedHashMap props = ItemizerProps.props(this, visible, drawWidth, drawHeight, drawBaseScale, drawScale); + long treeSignature = ItemizerProps.signature(this, props); + long layoutSignature = 0xcbf29ce484222325L; + layoutSignature = CachedUiScriptRuntime.mix(layoutSignature, drawX); + layoutSignature = CachedUiScriptRuntime.mix(layoutSignature, drawY); + layoutSignature = CachedUiScriptRuntime.mix(layoutSignature, drawWidth); + layoutSignature = CachedUiScriptRuntime.mix(layoutSignature, drawHeight); + UiRuntime baked = runtime.bake(moduleHandle, module, "itemizer", treeSignature, layoutSignature, drawWidth, drawHeight, fallback, drawX, drawY, drawWidth, drawHeight, () -> props); + if (baked == null) return; + baked.render(new UiRenderContext(renderer, fallback, ctx, tickDelta, UiProjectionMode.CURRENT)); + foregroundReady = true; + } + + @Override + public void renderEngineForeground(Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta, int screenW, int screenH) { + if (!foregroundReady || itemTasks.isEmpty()) { itemTasks.clear(); return; } + int seed = 0; + for (ItemRenderTask task : itemTasks) { + ItemizerRenderer.drawItem(renderer, task.stack(), task.x(), task.y(), task.scale(), seed++, task.overlayFlags(), task.alpha()); + } + itemTasks.clear(); + foregroundReady = false; + } + + void push(ItemizerEvent event, ItemStack stack, String label) { ItemizerModel.push(this, event, stack, label); } + + void setSize(float w, float h) { setBounds(getX(), getY(), w, h); } + + public enum ItemizerEvent implements EnumValue.IdProvider { + AUTO_EAT("auto_eat", "Eat"), AUTO_TOTEM("auto_totem", "Totem"), ELYTRA_SWAP("elytra_swap", "Swap"); + private final String id; final String fallbackLabel; + ItemizerEvent(String id, String fallbackLabel) { this.id = id; this.fallbackLabel = fallbackLabel; } + @Override public String id() { return id; } + } + + enum ItemizerDisplayMode implements EnumValue.IdProvider { + ICONS("icons"), COMPACT("compact"); + private final String id; ItemizerDisplayMode(String id) { this.id = id; } + @Override public String id() { return id; } + } + + enum ItemizerDirection implements EnumValue.IdProvider { + HORIZONTAL("horizontal"), VERTICAL("vertical"); + private final String id; ItemizerDirection(String id) { this.id = id; } + @Override public String id() { return id; } + } + + static final class Entry { + final ItemizerEvent event; + ItemStack stack = ItemStack.EMPTY; + String label; + float life; float totalLife; float anim; float punch; + Entry(ItemizerEvent event) { this.event = event; this.label = event.fallbackLabel; } + } + + record ItemRenderTask(ItemStack stack, float x, float y, float scale, int overlayFlags, float alpha) {} +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/MediaPlayer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/MediaPlayer.java new file mode 100644 index 0000000..3871366 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/MediaPlayer.java @@ -0,0 +1,174 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.combat; + +import dev.loki.lovisual.config.definition.SettingDef; +import dev.loki.lovisual.config.values.color.RGBAColorValue; +import dev.loki.lovisual.config.values.color.RGBColorValue; +import dev.loki.lovisual.config.values.mode.ModeValue; +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import dev.loki.lovisual.events.impl.render.RenderPrewarmCollectEvent; +import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; +import dev.loki.lovisual.features.gui.hud.registry.HudElementRegister; +import dev.loki.lovisual.render.engine.math.HudScale; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.font.info.FontInfo; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.util.media.session.MediaSessionService; +import dev.loki.lovisual.util.screen.ClientScreen; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.screens.ChatScreen; + +import java.util.List; + +@HudElementRegister(order = 70) +public final class MediaPlayer extends DraggableHudElement { + + { + defaultLayout(12.0f, 82.30222f); + } + + final HudGlobalConfig hud = HudGlobalConfig.get(); + final MediaSessionService mediaService = MediaSessionService.get(); + final NumberValue scale = new NumberValue<>("media_player_scale", 1.51, 0.5, 5.0); + final BooleanValue syncTheme = new BooleanValue("media_player_sync_theme", true); + final RGBAColorValue bg = new RGBAColorValue("media_player_bg", "#60000000"); + final NumberValue bgAlpha = new NumberValue<>("media_player_bg_alpha", 124, 0, 255); + final RGBColorValue accent = new RGBColorValue("media_player_accent", "#FF2F2F"); + final RGBColorValue text = new RGBColorValue("media_player_text", "#FFFFFF"); + final ModeValue bgEffect = new ModeValue("media_player_bg_effect", "None", MediaPlayerConstants.EFFECT_NONE, MediaPlayerConstants.EFFECT_BLUR, MediaPlayerConstants.EFFECT_GLASS); + final NumberValue blurAlpha = new NumberValue<>("media_player_blur_alpha", 255, 0, 255); + final BooleanValue gradient = new BooleanValue("media_player_gradient", true); + float anim = 0f; + MediaSessionService.Snapshot currentSnapshot = MediaSessionService.Snapshot.empty(MediaSessionService.isMediaAvailable()); + + float playHover; + float shuffleHover; + float prevHover; + float nextHover; + float repeatHover; + + float shuffleX; + float shuffleY; + float playX; + float playY; + float prevX; + float prevY; + float nextX; + float nextY; + float repeatX; + float repeatY; + float buttonSize; + int uiAccent; + int uiAccentSoft; + int uiText; + int uiTextMuted; + + public MediaPlayer() { + super("media_player", "Media Player", false); + } + + public void onResourceReload() { + mediaService.onResourceReload(); + currentSnapshot = MediaSessionService.Snapshot.empty(MediaSessionService.isMediaAvailable()); + } + + @Override + protected void defineSettings(List defs) { + defs.add(SettingDef.number(scale)); + defs.add(SettingDef.bool(syncTheme)); + defs.add(SettingDef.color(bg).visibleWhen(() -> !syncTheme.get() && !isGlassEffect())); + defs.add(SettingDef.number(bgAlpha).visibleWhen(() -> syncTheme.get() && !isGlassEffect())); + defs.add(SettingDef.colorNoAlpha(accent).visibleWhen(() -> !syncTheme.get())); + defs.add(SettingDef.colorNoAlpha(text).visibleWhen(() -> !syncTheme.get())); + defs.add(SettingDef.mode(bgEffect)); + defs.add(SettingDef.number(blurAlpha).visibleWhen(this::hasEffect)); + defs.add(SettingDef.bool(gradient).visibleWhen(() -> syncTheme.get() && !isGlassEffect())); + } + + @Override + public void applyDefaultPosition(int screenW, int screenH) { + float s = HudScale.scale(screenW, screenH) * scale.get().floatValue(); + x = 20f * s; + y = 20f * s; + } + + @Override + public boolean usesEngineRenderer() { + return true; + } + + @Override + public boolean isMouseOverInteractive(float mx, float my) { + if (!(ClientScreen.current() instanceof ChatScreen)) return false; + return MediaPlayerControls.isMouseOverInteractive(this, mx, my); + } + + @Override + public boolean onMouseClicked(float mx, float my, int button) { + if (!(ClientScreen.current() instanceof ChatScreen)) return false; + return MediaPlayerControls.handleClick(this, mx, my, button); + } + + @Override + public void renderEngine(Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta, int screenW, int screenH) { + Minecraft mc = Minecraft.getInstance(); + if (mc == null) return; + boolean chatOpen = ClientScreen.current() instanceof ChatScreen; + boolean preview = DraggableHudElementRegistry.isForceVisible(); + MediaSessionService.Snapshot snapshot = mediaService.snapshot(); + currentSnapshot = snapshot; + boolean shouldShow = snapshot.hasSession() || chatOpen || preview; + float target = shouldShow ? 1.0f : 0.0f; + anim = MediaPlayerConstants.lerp(anim, target, 0.25f); + if (!shouldShow && anim <= 0.02f) { + width = 0f; + height = 0f; + MediaPlayerControls.updateButtons(this); + return; + } + MediaPlayerRenderHelper.render(this, renderer, textRenderer, ctx, tickDelta, screenW, screenH); + } + + void setSize(float w, float h) { + width = w; + height = h; + } + + boolean hasEffect() { + return !MediaPlayerConstants.EFFECT_NONE.equals(bgEffect.get()); + } + + boolean isBlurEffect() { + return MediaPlayerConstants.EFFECT_BLUR.equals(bgEffect.get()); + } + + boolean isGlassEffect() { + return MediaPlayerConstants.EFFECT_GLASS.equals(bgEffect.get()); + } + + int getBgAlpha() { + if (syncTheme.get()) return bgAlpha.get(); + return (bg.getArgb() >>> 24) & 0xFF; + } + + @Override + public void collectRenderPrewarm(RenderPrewarmCollectEvent event) { + super.collectRenderPrewarm(event); + event.font("MediaPlayer", FontInfo.Type.Regular) + .font("Inter", FontInfo.Type.Bold) + .font("InterMedium", FontInfo.Type.Regular) + .svg("repeat") + .svg("repeat-1") + .svg("repeat-off"); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/inventory/InventoryConstants.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/inventory/InventoryConstants.java new file mode 100644 index 0000000..a8fc5eb --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/inventory/InventoryConstants.java @@ -0,0 +1,49 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.combat; + +final class InventoryConstants { + + private InventoryConstants() {} + + static final int COLS = 9; + static final int ROWS = 3; + static final int FIRST_INV_SLOT = 9; + static final int LAST_INV_SLOT = 36; + + static final float BASE_WIDTH = 123.0f; + static final float BASE_HEIGHT = 62.0f; + static final float HEADER_HEIGHT = 15.5f; + static final float BODY_Y_OFFSET = 18.4f; + static final float BODY_HEIGHT = 45.0f; + static final float PANEL_RADIUS = 4.0f; + static final float PANEL_SOFTNESS = 1.0f; + static final float PANEL_STROKE = 0.55f; + static final float HEADER_DIVIDER_X = 18.0f; + static final float HEADER_DIVIDER_Y = 5.0f; + static final float HEADER_DIVIDER_H = 6.0f; + static final float TITLE_ICON_X = 4.5f; + static final float TITLE_ICON_Y = 6.0f; + static final float TITLE_TEXT_X = 22.0f; + static final float TITLE_TEXT_Y = 6.5f; + static final float COUNT_LABEL_OFFSET = 21.0f; + static final float COUNT_VALUE_OFFSET = 2.0f; + static final float GRID_START_X = 4.0f; + static final float GRID_START_Y = 22.0f; + static final float GRID_STEP = 13.0f; + static final float GRID_LINE_OFFSET_X = 11.0f; + static final float GRID_LINE_OFFSET_Y = 10.0f; + static final float GRID_LINE_LENGTH = 9.0f; + static final float GRID_LINE_THICKNESS = 0.5f; + static final float ITEM_RENDER_SCALE = 0.5f; + + static final String COLOR_THEME = "Theme"; + static final String COLOR_CUSTOM = "Custom"; + static final String EFFECT_NONE = "None"; + static final String EFFECT_BLUR = "Blur"; +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/inventory/InventoryPalette.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/inventory/InventoryPalette.java new file mode 100644 index 0000000..59ec78c --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/inventory/InventoryPalette.java @@ -0,0 +1,70 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.combat; + +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; + +import static dev.loki.lovisual.features.theme.Theme.theme; + +final class InventoryPalette { + + private InventoryPalette() {} + + static void updatePalette(Inventory host) { + if (host.isThemeMode()) { + int alpha = host.bgAlpha.get(); + int window = HudRenderUtil.setAlpha(theme().windowBg(), alpha); + int header = HudRenderUtil.setAlpha( + HudRenderUtil.mixColor(theme().windowHeader(), theme().surface(), 0.18f), + Math.min(255, alpha + 14) + ); + int surface = HudRenderUtil.setAlpha( + HudRenderUtil.mixColor(theme().surface(), theme().windowBg(), 0.22f), + Math.min(255, alpha + 6) + ); + int deep = HudRenderUtil.setAlpha( + HudRenderUtil.mixColor(theme().surface(), theme().windowHeader(), 0.42f), + Math.min(255, alpha + 18) + ); + + host.uiHeaderLeft = HudRenderUtil.mixColor(header, window, 0.22f); + host.uiHeaderRight = HudRenderUtil.mixColor(surface, header, 0.52f); + host.uiBodyLeft = HudRenderUtil.mixColor(window, surface, 0.24f); + host.uiBodyRight = HudRenderUtil.mixColor(deep, surface, 0.18f); + host.uiOutline = HudRenderUtil.setAlpha( + HudRenderUtil.mixColor(theme().windowStroke(), theme().strokeSoft(), 0.18f), + Math.min(255, Math.max(182, alpha + 36)) + ); + host.uiText = theme().textPrimary(); + host.uiTitleText = HudRenderUtil.mixColor(theme().textPrimary(), theme().accent(), 0.14f); + host.uiMuted = HudRenderUtil.mixColor(theme().textMuted(), theme().textPrimary(), 0.16f); + host.uiCounter = HudRenderUtil.mixColor(theme().accent(), theme().textPrimary(), 0.38f); + host.uiDivider = HudRenderUtil.scaleAlpha(host.uiMuted, 0.68f); + host.uiGridDivider = HudRenderUtil.scaleAlpha(host.uiMuted, 0.28f); + host.uiBlurTint = HudRenderUtil.mixColor( + HudRenderUtil.mixColor(host.uiHeaderRight, host.uiBodyRight, 0.5f), + theme().accent(), + 0.14f + ); + return; + } + + host.uiHeaderLeft = host.bg.getArgb(); + host.uiHeaderRight = host.bg2.getArgb(); + host.uiBodyLeft = HudRenderUtil.mixColor(host.bg.getArgb(), host.bg2.getArgb(), 0.15f); + host.uiBodyRight = host.bg2.getArgb(); + host.uiOutline = host.stroke.getArgb(); + host.uiText = host.text.getArgb(); + host.uiTitleText = host.uiText; + host.uiMuted = host.muted.getArgb(); + host.uiCounter = HudRenderUtil.mixColor(host.text.getArgb(), 0xFFE1E1FF, 0.20f); + host.uiDivider = HudRenderUtil.scaleAlpha(host.uiMuted, 0.55f); + host.uiGridDivider = HudRenderUtil.scaleAlpha(host.uiMuted, 0.20f); + host.uiBlurTint = HudRenderUtil.mixColor(host.uiHeaderLeft, host.uiBodyRight, 0.5f); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/inventory/InventoryRenderHelper.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/inventory/InventoryRenderHelper.java new file mode 100644 index 0000000..4734966 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/inventory/InventoryRenderHelper.java @@ -0,0 +1,181 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.combat; + +import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; +import dev.loki.lovisual.features.gui.hud.draggable.impl.HudPanelLayoutModes; +import dev.loki.lovisual.features.gui.hud.draggable.impl.scripted.ScriptedInventoryHudPanel; +import dev.loki.lovisual.features.gui.hud.draggable.impl.scripted.ScriptedListHudPanel; +import dev.loki.lovisual.render.engine.math.HudScale; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.font.Fonts; +import dev.loki.lovisual.render.engine.text.font.info.FontInfo; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.util.input.KeyManager; +import dev.loki.lovisual.util.screen.ClientScreen; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.screens.ChatScreen; +import net.minecraft.util.Util; +import net.minecraft.world.item.ItemStack; + +import java.util.ArrayList; +import java.util.List; + +final class InventoryRenderHelper { + + private InventoryRenderHelper() {} + + static void handleTick(Inventory host) { + Minecraft mc = Minecraft.getInstance(); + if (!host.toggleInit) { + if (mc == null || mc.getWindow() == null) return; + host.toggleArmed = true; + String c = host.toggleBind.get(); + if (c != null && !c.isBlank() && !"NONE".equalsIgnoreCase(c) && ClientScreen.current() == null && KeyManager.isComboHeldAllowScreen(c)) host.toggleArmed = false; + host.toggleInit = true; + } + String combo = host.toggleBind.get(); + if (combo == null || combo.isBlank() || "NONE".equalsIgnoreCase(combo)) return; + if (mc == null || mc.getWindow() == null) return; + boolean pressed = ClientScreen.current() == null && KeyManager.isComboHeldAllowScreen(combo); + if (!pressed) { host.toggleArmed = true; return; } + if (host.toggleArmed) { host.displayVisible = !host.displayVisible; host.toggleArmed = false; } + } + + static List collectStacks(Inventory host) { + Minecraft mc = Minecraft.getInstance(); + List stacks = new ArrayList<>(InventoryConstants.LAST_INV_SLOT - InventoryConstants.FIRST_INV_SLOT); + if (mc == null || mc.player == null) { + return stacks; + } + for (int slot = InventoryConstants.FIRST_INV_SLOT; slot < InventoryConstants.LAST_INV_SLOT; slot++) { + stacks.add(mc.player.getInventory().getItem(slot)); + } + return stacks; + } + + static void render(Inventory host, Renderer2D renderer, TextRenderer textRenderer, + GuiGraphicsExtractor ctx, float tickDelta, int screenW, int screenH) { + Minecraft mc = Minecraft.getInstance(); + host.layoutReady = false; + host.hitboxActive = false; + host.pendingItemTasks.clear(); + + boolean forceVisible = DraggableHudElementRegistry.isForceVisible(); + if (mc == null || (mc.player == null && !forceVisible)) { + host.setSize(0.0f, 0.0f); + return; + } + if (!host.isEnabled() && !forceVisible) { + host.setSize(0.0f, 0.0f); + return; + } + boolean chatPreview = ClientScreen.current() instanceof ChatScreen; + List stacks = collectStacks(host); + boolean hasAnyItems = stacks.stream().anyMatch(stack -> stack != null && !stack.isEmpty()); + boolean displayTargetVisible = host.displayVisible; + boolean showWidget = forceVisible || (displayTargetVisible && (chatPreview || hasAnyItems)); + host.visibilityAnim = HudRenderUtil.animateVisibility(host.visibilityAnim, showWidget); + float widgetScale = HudRenderUtil.visibilityScale(host.visibilityAnim); + if (!showWidget && host.visibilityAnim <= 0.0f) { + host.setSize(0.0f, 0.0f); + return; + } + InventoryPalette.updatePalette(host); + + TextRenderer headerIconRenderer = Fonts.renderer("IconsNur", FontInfo.Type.Regular, TextRenderer.get()); + TextRenderer headerTextRenderer = Fonts.renderer("Inter", FontInfo.Type.Bold, TextRenderer.get()); + TextRenderer countRenderer = Fonts.renderer("OnestMedium", FontInfo.Type.Regular, TextRenderer.get()); + if (headerIconRenderer == null) headerIconRenderer = textRenderer; + if (headerTextRenderer == null) headerTextRenderer = textRenderer; + if (countRenderer == null) countRenderer = textRenderer; + + float baseScale = HudScale.scale(screenW, screenH) * 1.1f * HudPanelLayoutModes.effectiveScale(host.scaleValue); + float fontScale = 0.98f * (host.hud.getFontSize() / 18.0f); + long itemCount = 0L; + for (ItemStack stack : stacks) { + if (stack != null && !stack.isEmpty()) { + itemCount += stack.getCount(); + } + } + countRenderer.begin(fontScale * 0.92f, false, false); + String itemCountText = Long.toString(Math.max(0L, itemCount)); + float countLabelWidth = (float) countRenderer.getWidth("Items:", false); + float countValueWidth = (float) countRenderer.getWidth(itemCountText, false); + float countTextHeight = (float) countRenderer.getHeight(false); + countRenderer.end(); + + headerIconRenderer.begin(fontScale, false, false); + headerTextRenderer.begin(fontScale, false, false); + float headerIconH = (float) headerIconRenderer.getHeight(false); + float headerTextH = (float) headerTextRenderer.getHeight(false); + float titleWidth = (float) headerTextRenderer.getWidth("Inventory", false); + headerTextRenderer.end(); + headerIconRenderer.end(); + + float targetWidth = InventoryConstants.BASE_WIDTH * baseScale; + float headerWidth = (InventoryConstants.TITLE_TEXT_X * baseScale) + titleWidth + + countLabelWidth + countValueWidth + + ((InventoryConstants.COUNT_LABEL_OFFSET + 12.0f) * baseScale); + targetWidth = Math.max(targetWidth, headerWidth); + float targetHeight = InventoryConstants.BASE_HEIGHT * baseScale; + + host.displayWidth = HudRenderUtil.animateDimension(host.displayWidth, targetWidth); + host.displayHeight = HudRenderUtil.animateDimension(host.displayHeight, targetHeight); + host.setSize(host.displayWidth, host.displayHeight); + + float drawScale = Math.max(0.0f, widgetScale); + float drawWidth = host.displayWidth * drawScale; + float drawHeight = host.displayHeight * drawScale; + if (drawWidth <= 0.0f || drawHeight <= 0.0f) { + host.setSize(0.0f, 0.0f); + return; + } + float drawX = host.getX() + ((host.displayWidth - drawWidth) * 0.5f); + float drawY = host.getY() + ((host.displayHeight - drawHeight) * 0.5f); + float drawBaseScale = baseScale * drawScale; + float drawFontScale = fontScale * drawScale; + float drawHeaderIconH = headerIconH * drawScale; + float drawHeaderTextH = headerTextH * drawScale; + float drawCountTextHeight = countTextHeight * drawScale; + float drawCountLabelWidth = countLabelWidth * drawScale; + float drawCountValueWidth = countValueWidth * drawScale; + host.hitboxActive = forceVisible || showWidget; + + int resolvedHeaderIconColor = host.uiCounter; + if (host.headerIconPulse.get()) { + int pulsed = HudTextEffects.animatedColor(host.uiCounter, HudTextEffects.Effect.PULSE, + host.headerIconPulseSpeed.get(), (float) (Util.getMillis() / 1000.0), 0.0f); + resolvedHeaderIconColor = HudRenderUtil.mixColor(host.uiCounter, pulsed, host.headerIconPulseIntensity.get() / 100.0f); + } + host.scriptedPanel.render(renderer, textRenderer, ctx, tickDelta, + new ScriptedInventoryHudPanel.Panel(drawX, drawY, drawWidth, drawHeight, drawScale, + drawBaseScale, drawFontScale, drawHeaderIconH, drawHeaderTextH, + drawCountTextHeight, drawCountLabelWidth, drawCountValueWidth, itemCount, + host.hasEffect(), Math.min(1.0f, (host.blurAlpha.get() / 255.0f) * (host.isThemeMode() ? 1.15f : 1.0f)), + resolvedHeaderIconColor, host.uiGridDivider, HudPanelLayoutModes.current(host.layoutMode), + new ScriptedListHudPanel.Palette(host.uiHeaderLeft, host.uiHeaderRight, host.uiBodyLeft, host.uiBodyRight, + host.uiOutline, host.uiText, host.uiMuted, host.uiCounter, host.uiTitleText, host.uiDivider, host.uiBlurTint), + ScriptedInventoryHudPanel.cells(stacks))); + } + + static void renderForeground(Inventory host, Renderer2D renderer, TextRenderer textRenderer, + GuiGraphicsExtractor ctx, float tickDelta, int screenW, int screenH) { + if (!host.layoutReady || ctx == null || host.pendingItemTasks.isEmpty()) { + host.pendingItemTasks.clear(); + return; + } + for (Inventory.QueuedItemIcon task : host.pendingItemTasks) { + renderer.item(task.stack(), task.x(), task.y(), task.scale(), task.seed(), Renderer2D.ITEM_OVERLAY_ALL, null); + } + host.pendingItemTasks.clear(); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/itemizer/ItemizerModel.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/itemizer/ItemizerModel.java new file mode 100644 index 0000000..4c89186 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/itemizer/ItemizerModel.java @@ -0,0 +1,114 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.combat; + +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; + +import dev.loki.lovisual.render.engine.animation.AnimationUtility; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +final class ItemizerModel { + + private ItemizerModel() {} + + static void push(Itemizer host, Itemizer.ItemizerEvent event, ItemStack stack, String label) { + if (!accepts(host, event)) return; + Itemizer.Entry entry = host.entries.remove(event); + if (entry == null) { + entry = new Itemizer.Entry(event); + } + entry.stack = stack.copy(); + entry.label = label != null && !label.isBlank() ? label : event.fallbackLabel; + entry.life = Math.max(0.1f, host.duration.get().floatValue()); + entry.totalLife = entry.life; + entry.punch = 1.0f; + host.entries.put(event, entry); + trimInternalEntries(host); + } + + static boolean accepts(Itemizer host, Itemizer.ItemizerEvent event) { + return switch (event) { + case AUTO_EAT -> host.showAutoEat.get(); + case AUTO_TOTEM -> host.showAutoTotem.get(); + case ELYTRA_SWAP -> host.showElytraSwap.get(); + }; + } + + static void trimInternalEntries(Itemizer host) { + while (host.entries.size() > Itemizer.MAX_INTERNAL_ENTRIES) { + Itemizer.ItemizerEvent first = host.entries.keySet().iterator().next(); + host.entries.remove(first); + } + } + + static List updateEntries(Itemizer host, float dt) { + List remove = new ArrayList<>(); + List out = new ArrayList<>(); + for (Map.Entry mapEntry : host.entries.entrySet()) { + Itemizer.Entry entry = mapEntry.getValue(); + entry.life = Math.max(0.0f, entry.life - dt); + entry.punch = AnimationUtility.approach(entry.punch, 0.0f, dt, 8.0f); + boolean alive = entry.life > 0.0f; + entry.anim = AnimationUtility.approach(entry.anim, alive ? 1.0f : 0.0f, dt, 13.0f); + entry.anim = AnimationUtility.snap(entry.anim, alive ? 1.0f : 0.0f, 0.002f); + if (!alive && entry.anim <= 0.0f) { + remove.add(mapEntry.getKey()); + continue; + } + if (out.size() < host.maxEntries.get()) { + out.add(entry); + } + } + for (Itemizer.ItemizerEvent event : remove) { + host.entries.remove(event); + } + return out; + } + + static List previewEntries(Itemizer host) { + Itemizer.Entry eat = new Itemizer.Entry(Itemizer.ItemizerEvent.AUTO_EAT); + eat.stack = new ItemStack(Items.COOKED_BEEF); + eat.label = "Eat"; + eat.life = 1.0f; + eat.totalLife = 1.0f; + eat.anim = 1.0f; + + Itemizer.Entry totem = new Itemizer.Entry(Itemizer.ItemizerEvent.AUTO_TOTEM); + totem.stack = new ItemStack(Items.TOTEM_OF_UNDYING); + totem.label = "Totem"; + totem.life = 1.0f; + totem.totalLife = 1.0f; + totem.anim = 1.0f; + + Itemizer.Entry swap = new Itemizer.Entry(Itemizer.ItemizerEvent.ELYTRA_SWAP); + swap.stack = new ItemStack(Items.ELYTRA); + swap.label = "Swap"; + swap.life = 1.0f; + swap.totalLife = 1.0f; + swap.anim = 1.0f; + + List list = new ArrayList<>(); + list.add(eat); + list.add(totem); + list.add(swap); + return list.subList(0, Math.min(host.maxEntries.get(), list.size())); + } + + static float baseEntryWidth(Itemizer host) { + return host.displayMode.get() == Itemizer.ItemizerDisplayMode.COMPACT ? Itemizer.BASE_COMPACT_W : Itemizer.BASE_ICON_CARD; + } + + static float baseEntryHeight(Itemizer host) { + return host.displayMode.get() == Itemizer.ItemizerDisplayMode.COMPACT ? Itemizer.BASE_COMPACT_H : Itemizer.BASE_ICON_CARD; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/itemizer/ItemizerProps.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/itemizer/ItemizerProps.java new file mode 100644 index 0000000..f9c3b66 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/itemizer/ItemizerProps.java @@ -0,0 +1,153 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.combat; + +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; +import dev.loki.lovisual.features.gui.hud.script.HudScriptLayouts; +import dev.loki.lovisual.features.theme.Theme; +import dev.loki.lovisual.features.theme.Themes; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.math.ColorMath; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.module.UiScriptModule; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.runtime.CachedUiScriptRuntime; +import net.minecraft.client.Minecraft; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.item.ItemStack; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +final class ItemizerProps { + + private ItemizerProps() {} + + static UiScriptModule ensureModule(Itemizer host) { + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.getResourceManager() == null) return null; + HudScriptLayouts.pollReloadCombo(mc); + if (host.moduleHandle.consumeChanged()) { + host.runtime.reset(); + } + if (!host.moduleHandle.ensureLoaded(mc.getResourceManager())) { + HudScriptLayouts.reportLoadError(host.moduleHandle); + return null; + } + host.moduleHandle.consumeChanged(); + return host.moduleHandle.module(); + } + + static LinkedHashMap props(Itemizer host, List visible, float width, float height, float baseScale, float drawScale) { + LinkedHashMap out = new LinkedHashMap<>(); + out.put("id", "itemizer"); + out.put("width", width); + out.put("height", height); + out.put("baseScale", baseScale); + out.put("drawScale", drawScale); + out.put("mode", host.displayMode.get().id()); + out.put("direction", host.direction.get().id()); + out.put("entryWidth", ItemizerModel.baseEntryWidth(host) * baseScale); + out.put("entryHeight", ItemizerModel.baseEntryHeight(host) * baseScale); + out.put("gap", Itemizer.BASE_GAP * baseScale); + out.put("blur", host.blur.get()); + out.put("palette", paletteProps(host)); + List> itemProps = new ArrayList<>(); + for (Itemizer.Entry entry : visible) { + itemProps.add(entryProps(host, entry)); + } + out.put("items", itemProps.toArray()); + return out; + } + + static LinkedHashMap paletteProps(Itemizer host) { + Themes.Theme t = Theme.theme(); + int alpha = host.bgAlpha.get(); + int bg1 = HudRenderUtil.setAlpha(HudRenderUtil.mixColor(t.windowBg(), t.surface(), 0.24f), alpha); + int bg2 = HudRenderUtil.setAlpha(HudRenderUtil.mixColor(t.surface(), 0xFF000000, 0.22f), Math.min(255, alpha + 18)); + int stroke = HudRenderUtil.setAlpha(HudRenderUtil.mixColor(t.windowStroke(), t.accentSoft(), 0.22f), 176); + LinkedHashMap out = new LinkedHashMap<>(); + out.put("bg1", hex(bg1)); + out.put("bg2", hex(bg2)); + out.put("stroke", hex(stroke)); + out.put("text", hex(t.textPrimary())); + out.put("muted", hex(t.textMuted())); + out.put("accent", hex(t.accent())); + out.put("glow", hex(HudRenderUtil.setAlpha(t.accent(), 155))); + out.put("blurAlpha", Math.min(1.0f, alpha / 255.0f)); + return out; + } + + static LinkedHashMap entryProps(Itemizer host, Itemizer.Entry entry) { + ItemStack stack = entry.stack != null ? entry.stack : ItemStack.EMPTY; + LinkedHashMap out = new LinkedHashMap<>(); + out.put("key", entry.event.id()); + out.put("kind", entry.event.id()); + out.put("label", entry.label); + out.put("stack", stack.copy()); + out.put("item", stack.isEmpty() ? "" : BuiltInRegistries.ITEM.getKey(stack.getItem()).toString()); + out.put("count", stack.isEmpty() ? 1 : stack.getCount()); + out.put("damage", stack.isEmpty() ? 0 : stack.getDamageValue()); + out.put("maxDamage", stack.isEmpty() ? 0 : stack.getMaxDamage()); + out.put("alpha", AnimationUtility.clamp01(entry.anim)); + out.put("life", entry.totalLife <= 0.0f ? 1.0f : AnimationUtility.clamp01(entry.life / entry.totalLife)); + out.put("pulse", AnimationUtility.clamp01(entry.punch)); + out.put("accent", hex(entryAccent(host, entry.event))); + return out; + } + + static int entryAccent(Itemizer host, Itemizer.ItemizerEvent event) { + Themes.Theme t = Theme.theme(); + int target = switch (event) { + case AUTO_EAT -> 0xFF74E083; + case AUTO_TOTEM -> 0xFFFFD15C; + case ELYTRA_SWAP -> 0xFF77D7FF; + }; + return ColorMath.colorWithAlpha(HudRenderUtil.mixColor(t.accent(), target, 0.72f), 255); + } + + static long signature(Itemizer host, Map props) { + long h = 0xcbf29ce484222325L; + h = CachedUiScriptRuntime.mix(h, host.displayMode.get().id()); + h = CachedUiScriptRuntime.mix(h, host.direction.get().id()); + h = CachedUiScriptRuntime.mix(h, host.getWidth()); + h = CachedUiScriptRuntime.mix(h, host.getHeight()); + h = CachedUiScriptRuntime.mix(h, host.bgAlpha.get()); + h = CachedUiScriptRuntime.mix(h, host.blur.get()); + Object itemsValue = props.get("items"); + Object[] items = itemsValue instanceof Object[] arr ? arr : new Object[0]; + h = CachedUiScriptRuntime.mix(h, items.length); + for (Object itemValue : items) { + if (!(itemValue instanceof Map item)) continue; + h = CachedUiScriptRuntime.mix(h, string(item.get("key"))); + h = CachedUiScriptRuntime.mix(h, string(item.get("label"))); + h = CachedUiScriptRuntime.mix(h, string(item.get("item"))); + h = CachedUiScriptRuntime.mix(h, intValue(item.get("count"))); + h = CachedUiScriptRuntime.mix(h, Math.round(floatValue(item.get("alpha")) * 1000.0f)); + h = CachedUiScriptRuntime.mix(h, Math.round(floatValue(item.get("life")) * 1000.0f)); + h = CachedUiScriptRuntime.mix(h, Math.round(floatValue(item.get("pulse")) * 1000.0f)); + } + return h; + } + + static String hex(int argb) { + return String.format("#%08X", argb); + } + + static String string(Object value) { + return value instanceof String s ? s : ""; + } + + static int intValue(Object value) { + return value instanceof Number n ? n.intValue() : 0; + } + + static float floatValue(Object value) { + return value instanceof Number n ? n.floatValue() : 0.0f; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/itemizer/ItemizerRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/itemizer/ItemizerRenderer.java new file mode 100644 index 0000000..45a3cb2 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/itemizer/ItemizerRenderer.java @@ -0,0 +1,108 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.combat; + +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.helpers.clip.ClipFunction; +import net.minecraft.world.item.ItemStack; + +import java.util.List; + +final class ItemizerRenderer { + + private ItemizerRenderer() {} + + static void drawCards(Itemizer host, Renderer2D renderer, List visible, float x, float y, float scale, float globalAlpha) { + if (renderer == null || visible == null || visible.isEmpty()) return; + float entryW = ItemizerModel.baseEntryWidth(host) * scale; + float entryH = ItemizerModel.baseEntryHeight(host) * scale; + float gap = Itemizer.BASE_GAP * scale; + for (int i = 0; i < visible.size(); i++) { + Itemizer.Entry entry = visible.get(i); + float cardX = x; + float cardY = y; + if (host.direction.get() == Itemizer.ItemizerDirection.HORIZONTAL) { + cardX += i * (entryW + gap); + } else { + cardY += i * (entryH + gap); + } + float alpha = AnimationUtility.clamp01(globalAlpha * entry.anim); + if (alpha <= 0.001f) continue; + float radius = Math.max(3.0f * scale, Math.min(entryW, entryH) * 0.24f); + drawGlassCard(host, renderer, cardX, cardY, entryW, entryH, radius, scale, alpha); + drawClippedTimeStrip(host, renderer, entry, cardX, cardY, entryW, entryH, radius, scale, alpha); + queueItem(host, entry, cardX, cardY, entryW, entryH, scale, alpha); + } + } + + static void drawGlassCard(Itemizer host, Renderer2D renderer, float x, float y, float width, float height, float radius, float scale, float alpha) { + if (width <= 0.0f || height <= 0.0f || alpha <= 0.001f) return; + float blurStrength = host.blur.get() ? (host.bgAlpha.get() / 255.0f) * alpha : 0.0f; + HudRenderUtil.drawLiquidGlass(x, y, width, height, radius, Math.max(0.001f, scale), true, blurStrength, alpha); + int veil = HudRenderUtil.glassSmallBackground(alpha); + renderer.roundedRectCorners(x, y, width, height, radius, radius, radius, radius, 1.0f, veil); + } + + static void drawClippedTimeStrip(Itemizer host, Renderer2D renderer, Itemizer.Entry entry, float x, float y, float width, float height, float cardRadius, float scale, float alpha) { + boolean clipped = ClipFunction.pushRoundedRect(x, y, width, height, cardRadius); + if (!clipped) return; + try { + drawTimeStrip(host, renderer, entry, x, y, width, height, cardRadius, scale, alpha); + } finally { + ClipFunction.pop(); + } + } + + static void drawTimeStrip(Itemizer host, Renderer2D renderer, Itemizer.Entry entry, float x, float y, float width, float height, float cardRadius, float scale, float alpha) { + if (renderer == null || entry == null || width <= 0.0f || height <= 0.0f || alpha <= 0.001f) return; + float life = entry.totalLife <= 0.0f ? 1.0f : AnimationUtility.clamp01(entry.life / entry.totalLife); + if (life <= 0.001f) return; + float stripH = Math.max(1.45f * scale, Math.min(3.0f * scale, height * 0.13f)); + float bleed = Math.max(0.65f, 0.45f * scale); + float stripDrawH = stripH + bleed; + float stripY = y + height - stripH; + float bottomR = Math.min(cardRadius, stripDrawH); + int accent = ItemizerProps.entryAccent(host, entry.event); + int trackTop = HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(0xFF07090D, accent, 0.08f), 0.30f * alpha); + int trackBottom = HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(0xFF020305, accent, 0.18f), 0.48f * alpha); + renderer.roundedRectCornersQuad(x, stripY, width, stripDrawH, 0.0f, 0.0f, bottomR, bottomR, 0.0f, trackTop, trackTop, trackBottom, trackBottom); + float fillW = Math.max(0.0f, width * life); + if (fillW <= 0.35f) return; + float fillRLeft = Math.min(bottomR, fillW * 0.5f); + float fillRRight = life >= 0.985f ? bottomR : Math.min(stripDrawH * 0.5f, fillW * 0.5f); + int fillTopLeft = HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(accent, 0xFFFFFFFF, 0.28f), 0.92f * alpha); + int fillTopRight = HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(accent, 0xFFFFFFFF, 0.10f), 0.84f * alpha); + int fillBottomRight = HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(accent, 0xFF05070A, 0.22f), 0.78f * alpha); + int fillBottomLeft = HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(accent, 0xFFFFFFFF, 0.16f), 0.88f * alpha); + renderer.roundedRectCornersQuad(x, stripY, fillW, stripDrawH, 0.0f, life >= 0.985f ? 0.0f : fillRRight, fillRRight, fillRLeft, 0.0f, fillTopLeft, fillTopRight, fillBottomRight, fillBottomLeft); + } + + static void queueItem(Itemizer host, Itemizer.Entry entry, float cardX, float cardY, float cardW, float cardH, float scale, float alpha) { + if (entry == null || entry.stack == null || entry.stack.isEmpty()) return; + boolean compact = host.displayMode.get() == Itemizer.ItemizerDisplayMode.COMPACT; + float iconSize = compact ? Math.min(16.0f * scale, cardH - 7.0f * scale) : Math.min(Math.min(16.0f * scale, cardW - 8.0f * scale), cardH - 8.0f * scale); + if (iconSize <= 0.0f) return; + float iconX = compact ? cardX + 4.5f * scale : cardX + (cardW - iconSize) * 0.5f; + float iconY = cardY + (cardH - iconSize) * 0.5f - 0.5f * scale; + int overlayFlags = compact ? Renderer2D.ITEM_OVERLAY_ALL : Renderer2D.ITEM_OVERLAY_NONE; + host.itemTasks.add(new Itemizer.ItemRenderTask(entry.stack.copy(), iconX, iconY, Math.max(0.05f, iconSize / 16.0f), overlayFlags, alpha)); + } + + static void drawItem(Renderer2D renderer, ItemStack stack, float x, float y, float itemScale, int seed, int overlayFlags, float alpha) { + if (renderer == null || stack == null || stack.isEmpty() || alpha <= 0.001f) return; + double previousAlpha = renderer.getAlpha(); + renderer.setAlpha(previousAlpha * AnimationUtility.clamp01(alpha)); + try { + renderer.item(stack, x, y, itemScale, seed, overlayFlags, null); + } finally { + renderer.setAlpha(previousAlpha); + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/mediaplayer/MediaPlayerConstants.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/mediaplayer/MediaPlayerConstants.java new file mode 100644 index 0000000..831bdd4 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/mediaplayer/MediaPlayerConstants.java @@ -0,0 +1,76 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.combat; + +import dev.loki.lovisual.render.engine.text.font.Fonts; +import dev.loki.lovisual.render.engine.text.font.info.FontInfo; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; + +final class MediaPlayerConstants { + + static final float BASE_PADDING = 6f; + static final float BASE_GAP = 6f; + static final float BASE_ART_SIZE = 30f; + static final float BASE_ART_BOOST = 3f; + static final float BASE_ART_OVERHANG = 4f; + static final float BASE_RADIUS = 6f; + static final float BASE_BAR_H = 2.4f; + static final float BASE_BUTTON_SIZE = 16f; + static final float BASE_BUTTON_GAP = 8f; + static final float BASE_TEXT_GAP = 2f; + static final float BASE_BAR_GAP = 4f; + static final float BASE_BOTTOM_GAP = 4f; + static final float BASE_TITLE_SCROLL_SPEED = 22f; + static final float BASE_TITLE_SCROLL_GAP = 14f; + static final float BASE_TITLE_FADE = 12f; + static final float TITLE_SCROLL_PAUSE_SEC = 0.8f; + static final String EFFECT_NONE = "None"; + static final String EFFECT_BLUR = "Blur"; + static final String EFFECT_GLASS = "Glass"; + static final float BASE_CONTENT_W = 170f; + static final int ICON_PAUSE = 0xEA02; + static final int ICON_PLAY = 0xEA03; + static final int ICON_PREV = 0xEA04; + static final int ICON_NEXT = 0xEA05; + static final int ICON_SHUFFLE = 0xEA06; + + private MediaPlayerConstants() {} + + static String iconString(int codepoint) { + return new String(Character.toChars(codepoint)); + } + + static TextRenderer getMediaIcons(TextRenderer fallback) { + return Fonts.renderer("MediaPlayer", FontInfo.Type.Regular, fallback); + } + + static String safe(String value) { + if (value == null) return ""; + String v = value.trim(); + return v.isEmpty() ? "" : v; + } + + static String formatTime(long seconds) { + long sec = Math.max(0L, seconds); + long m = sec / 60L; + long s = sec % 60L; + return String.format("%d:%02d", m, s); + } + + static float lerp(float a, float b, float t) { + return a + (b - a) * t; + } + + static boolean hit(float mx, float my, float x, float y, float w, float h) { + return mx >= x && mx <= x + w && my >= y && my <= y + h; + } + + static float clamp01(float v) { + return Math.max(0f, Math.min(1f, v)); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/mediaplayer/MediaPlayerControls.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/mediaplayer/MediaPlayerControls.java new file mode 100644 index 0000000..83ee815 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/mediaplayer/MediaPlayerControls.java @@ -0,0 +1,196 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.combat; + +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; +import dev.loki.lovisual.render.engine.math.HudScale; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.svg.model.SvgRenderOptions; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.render.engine.color.RenderColor; +import dev.loki.lovisual.util.media.info.RepeatMode; +import dev.loki.lovisual.util.media.session.MediaSessionService; +import net.minecraft.client.Minecraft; + +final class MediaPlayerControls { + + private MediaPlayerControls() {} + + static boolean isMouseOverInteractive(MediaPlayer p, float mx, float my) { + if (p.buttonSize <= 0f) return false; + boolean showShuffle = p.currentSnapshot != null && p.currentSnapshot.supportsShuffle(); + boolean showRepeat = p.currentSnapshot != null && p.currentSnapshot.supportsRepeat(); + return MediaPlayerConstants.hit(mx, my, p.playX, p.playY, p.buttonSize, p.buttonSize) + || (showShuffle && MediaPlayerConstants.hit(mx, my, p.shuffleX, p.shuffleY, p.buttonSize, p.buttonSize)) + || MediaPlayerConstants.hit(mx, my, p.prevX, p.prevY, p.buttonSize, p.buttonSize) + || MediaPlayerConstants.hit(mx, my, p.nextX, p.nextY, p.buttonSize, p.buttonSize) + || (showRepeat && MediaPlayerConstants.hit(mx, my, p.repeatX, p.repeatY, p.buttonSize, p.buttonSize)); + } + + static boolean handleClick(MediaPlayer p, float mx, float my, int button) { + if (p.currentSnapshot == null || !p.currentSnapshot.hasSession()) return false; + if (button != 0) return false; + try { + if (p.currentSnapshot.supportsShuffle() && MediaPlayerConstants.hit(mx, my, p.shuffleX, p.shuffleY, p.buttonSize, p.buttonSize)) { + p.mediaService.toggleShuffle(); + return true; + } + if (MediaPlayerConstants.hit(mx, my, p.playX, p.playY, p.buttonSize, p.buttonSize)) { + p.mediaService.playPause(); + return true; + } + if (MediaPlayerConstants.hit(mx, my, p.prevX, p.prevY, p.buttonSize, p.buttonSize)) { + p.mediaService.previous(); + return true; + } + if (MediaPlayerConstants.hit(mx, my, p.nextX, p.nextY, p.buttonSize, p.buttonSize)) { + p.mediaService.next(); + return true; + } + if (p.currentSnapshot.supportsRepeat() && MediaPlayerConstants.hit(mx, my, p.repeatX, p.repeatY, p.buttonSize, p.buttonSize)) { + p.mediaService.cycleRepeatMode(); + return true; + } + } catch (Throwable ignored) { + } + return false; + } + + static void updateButtons(MediaPlayer p) { + p.shuffleX = 0f; + p.shuffleY = 0f; + p.playX = 0f; + p.playY = 0f; + p.prevX = 0f; + p.prevY = 0f; + p.nextX = 0f; + p.nextY = 0f; + p.repeatX = 0f; + p.repeatY = 0f; + p.buttonSize = 0f; + p.playHover = 0f; + p.shuffleHover = 0f; + p.prevHover = 0f; + p.nextHover = 0f; + p.repeatHover = 0f; + } + + static void updateHover(MediaPlayer p, boolean chatOpen, boolean showShuffle, boolean showRepeat) { + if (!chatOpen) { + p.playHover = MediaPlayerConstants.lerp(p.playHover, 0f, 0.2f); + p.shuffleHover = MediaPlayerConstants.lerp(p.shuffleHover, 0f, 0.2f); + p.prevHover = MediaPlayerConstants.lerp(p.prevHover, 0f, 0.2f); + p.nextHover = MediaPlayerConstants.lerp(p.nextHover, 0f, 0.2f); + p.repeatHover = MediaPlayerConstants.lerp(p.repeatHover, 0f, 0.2f); + return; + } + Minecraft mc = Minecraft.getInstance(); + int fbw = mc.getWindow().getWidth(); + int fbh = mc.getWindow().getHeight(); + float uiScale = HudScale.scale(fbw, fbh); + float mx = HudScale.toVirtual((float) mc.mouseHandler.xpos(), uiScale); + float my = HudScale.toVirtual((float) mc.mouseHandler.ypos(), uiScale); + p.shuffleHover = MediaPlayerConstants.lerp(p.shuffleHover, showShuffle && MediaPlayerConstants.hit(mx, my, p.shuffleX, p.shuffleY, p.buttonSize, p.buttonSize) ? 1f : 0f, 0.25f); + p.playHover = MediaPlayerConstants.lerp(p.playHover, MediaPlayerConstants.hit(mx, my, p.playX, p.playY, p.buttonSize, p.buttonSize) ? 1f : 0f, 0.25f); + p.prevHover = MediaPlayerConstants.lerp(p.prevHover, MediaPlayerConstants.hit(mx, my, p.prevX, p.prevY, p.buttonSize, p.buttonSize) ? 1f : 0f, 0.25f); + p.nextHover = MediaPlayerConstants.lerp(p.nextHover, MediaPlayerConstants.hit(mx, my, p.nextX, p.nextY, p.buttonSize, p.buttonSize) ? 1f : 0f, 0.25f); + p.repeatHover = MediaPlayerConstants.lerp(p.repeatHover, showRepeat && MediaPlayerConstants.hit(mx, my, p.repeatX, p.repeatY, p.buttonSize, p.buttonSize) ? 1f : 0f, 0.25f); + } + + static void drawControl(Renderer2D renderer, TextRenderer iconRenderer, String icon, float x, float y, float size, float hover, float alpha, float iconScale, int iconColor, MediaPlayer p) { + if (size <= 0f) return; + float radius = size * 0.5f; + float hoverEase = MediaPlayerConstants.clamp01(hover); + int bg = HudRenderUtil.scaleAlpha(p.uiAccentSoft, hoverEase * 0.45f * alpha); + if (hoverEase > 0.01f) { + renderer.roundedRect(x, y, size, size, radius, 1.0f, bg); + } + iconRenderer.begin(iconScale * alpha, false, false); + float iw = (float) iconRenderer.getWidth(icon, false); + float ih = (float) iconRenderer.getHeight(false); + float ix = x + (size - iw) * 0.5f; + float iy = y + (size - ih) * 0.5f; + iconRenderer.render(icon, ix, iy, new RenderColor(iconColor), false); + iconRenderer.end(); + } + + static void drawRepeatControl(Renderer2D renderer, MediaSessionService.Snapshot snapshot, float x, float y, float size, float hover, float alpha, MediaPlayer p) { + if (size <= 0f) return; + float radius = size * 0.5f; + float hoverEase = MediaPlayerConstants.clamp01(hover); + int bg = HudRenderUtil.scaleAlpha(p.uiAccentSoft, hoverEase * 0.45f * alpha); + if (hoverEase > 0.01f) { + renderer.roundedRect(x, y, size, size, radius, 1.0f, bg); + } + String icon = repeatSvg(snapshot); + int color = repeatColor(snapshot, alpha); + float iconSize = size * 0.78f; + float ix = x + (size - iconSize) * 0.5f; + float iy = y + (size - iconSize) * 0.5f; + renderer.svg(icon, ix, iy, iconSize, iconSize, SvgRenderOptions.overrideColor(color)); + } + + static int shuffleColor(MediaSessionService.Snapshot snapshot, float alpha) { + if (snapshot == null || !snapshot.supportsShuffle()) { + return HudRenderUtil.scaleAlpha(0xFF000000, 0.36f * alpha); + } + return snapshot.isShuffleActive() + ? HudRenderUtil.scaleAlpha(0xFFFFFFFF, alpha) + : HudRenderUtil.scaleAlpha(0xFFB7BEC8, 0.48f * alpha); + } + + static int repeatColor(MediaSessionService.Snapshot snapshot, float alpha) { + if (snapshot == null || !snapshot.supportsRepeat()) { + return HudRenderUtil.scaleAlpha(0xFF000000, 0.36f * alpha); + } + RepeatMode mode = snapshot.repeatMode(); + return mode == RepeatMode.ALL || mode == RepeatMode.ONE + ? HudRenderUtil.scaleAlpha(0xFFFFFFFF, alpha) + : HudRenderUtil.scaleAlpha(0xFFB7BEC8, 0.48f * alpha); + } + + static String repeatSvg(MediaSessionService.Snapshot snapshot) { + if (snapshot == null || !snapshot.supportsRepeat()) { + return "repeat-off"; + } + return snapshot.repeatMode() == RepeatMode.ONE ? "repeat-1" : "repeat"; + } + + static void layoutControls(MediaPlayer p, float drawX, float drawY, float textX, float contentW, float bottomY, float metaH, float btnSizeBase, float buttonGap, float baseScale, boolean showShuffle, boolean showRepeat, float scale) { + float active = 3 + (showShuffle ? 1 : 0) + (showRepeat ? 1 : 0); + float reserved = btnSizeBase * active + buttonGap * Math.max(0, active - 1); + float controlsX = textX + (contentW - reserved) * 0.5f; + float controlsY = bottomY + (metaH - btnSizeBase) * 0.5f; + float cursorX = controlsX; + if (showShuffle) { + p.shuffleX = drawX + cursorX * scale; + p.shuffleY = drawY + controlsY * scale; + cursorX += btnSizeBase + buttonGap; + } else { + p.shuffleX = 0f; + p.shuffleY = 0f; + } + p.prevX = drawX + cursorX * scale; + p.prevY = drawY + controlsY * scale; + cursorX += btnSizeBase + buttonGap; + p.playX = drawX + cursorX * scale; + p.playY = p.prevY; + cursorX += btnSizeBase + buttonGap; + p.nextX = drawX + cursorX * scale; + p.nextY = p.prevY; + cursorX += btnSizeBase + buttonGap; + if (showRepeat) { + p.repeatX = drawX + cursorX * scale; + p.repeatY = p.prevY; + } else { + p.repeatX = 0f; + p.repeatY = 0f; + } + p.buttonSize = btnSizeBase * scale; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/mediaplayer/MediaPlayerRenderHelper.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/mediaplayer/MediaPlayerRenderHelper.java new file mode 100644 index 0000000..dd1f47b --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/mediaplayer/MediaPlayerRenderHelper.java @@ -0,0 +1,156 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.combat; + +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; +import dev.loki.lovisual.render.engine.math.HudScale; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.font.Fonts; +import dev.loki.lovisual.render.engine.text.font.info.FontInfo; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.util.media.info.MediaInfo; +import dev.loki.lovisual.util.media.session.MediaSessionService; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; + +import static dev.loki.lovisual.features.theme.Theme.theme; + +final class MediaPlayerRenderHelper { + + private MediaPlayerRenderHelper() {} + + static void render(MediaPlayer p, Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta, int screenW, int screenH) { + Minecraft mc = Minecraft.getInstance(); + if (mc == null) return; + MediaSessionService.Snapshot snapshot = p.mediaService.snapshot(); + p.currentSnapshot = snapshot; + MediaInfo media = snapshot.media(); + boolean hasSession = snapshot.hasSession(); + boolean mediaAvailable = snapshot.mediaAvailable(); + + TextRenderer titleRenderer = Fonts.renderer("Inter", FontInfo.Type.Bold, textRenderer); + TextRenderer artistRenderer = Fonts.renderer("InterMedium", FontInfo.Type.Regular, textRenderer); + TextRenderer metaRenderer = Fonts.renderer("Inter", FontInfo.Type.Regular, textRenderer); + TextRenderer iconRenderer = MediaPlayerConstants.getMediaIcons(textRenderer); + + float mediaScale = p.scale.get().floatValue(); + float baseScale = HudScale.scale(screenW, screenH) * mediaScale; + float fontScale = (p.hud.getFontSize() / 18f) * mediaScale; + float padding = MediaPlayerConstants.BASE_PADDING * baseScale; + float gap = MediaPlayerConstants.BASE_GAP * baseScale; + float artBase = MediaPlayerConstants.BASE_ART_SIZE * baseScale; + float artBoost = MediaPlayerConstants.BASE_ART_BOOST * baseScale; + float artOverhang = MediaPlayerConstants.BASE_ART_OVERHANG * baseScale; + float radius = MediaPlayerConstants.BASE_RADIUS * baseScale; + float barH = MediaPlayerConstants.BASE_BAR_H * baseScale; + float textGap = MediaPlayerConstants.BASE_TEXT_GAP * baseScale; + float barGap = MediaPlayerConstants.BASE_BAR_GAP * baseScale; + float bottomGap = MediaPlayerConstants.BASE_BOTTOM_GAP * baseScale; + float buttonGap = MediaPlayerConstants.BASE_BUTTON_GAP * baseScale; + float contentW = MediaPlayerConstants.BASE_CONTENT_W * baseScale; + + float titleScale = 0.95f * fontScale; + float artistScale = 0.78f * fontScale; + float metaScale = 0.70f * fontScale; + float iconScale = 0.9f * fontScale; + + String title = hasSession ? MediaPlayerConstants.safe(media.title()) : ""; + String artist = hasSession ? MediaPlayerConstants.safe(media.artist()) : ""; + String owner = hasSession && snapshot.session() != null ? MediaPlayerConstants.safe(snapshot.session().getOwner()) : ""; + long predictedPos = hasSession ? snapshot.predictedPositionSeconds() : 0L; + String timeText = hasSession ? MediaPlayerConstants.formatTime(predictedPos) : ""; + + metaRenderer.begin(metaScale, false, false); + float timeW = hasSession ? (float) metaRenderer.getWidth(timeText, false) : 0f; + metaRenderer.end(); + + boolean showShuffle = hasSession && snapshot.supportsShuffle(); + boolean showRepeat = hasSession && snapshot.supportsRepeat(); + float btnSizeBase = MediaPlayerConstants.BASE_BUTTON_SIZE * baseScale; + + float maxOwnerW = Math.max(0f, contentW - timeW - buttonGap); + String ownerText = hasSession ? MediaPlayerTextHelper.trimText(metaRenderer, owner, maxOwnerW, metaScale) : ""; + String titleText = hasSession ? title : ""; + String artistText = hasSession ? MediaPlayerTextHelper.trimText(artistRenderer, artist, contentW, artistScale) : ""; + + titleRenderer.begin(titleScale, false, false); + float titleW = hasSession ? (float) titleRenderer.getWidth(titleText, false) : 0f; + float titleH = (float) titleRenderer.getHeight(false); + titleRenderer.end(); + + artistRenderer.begin(artistScale, false, false); + if (hasSession) artistRenderer.getWidth(artistText, false); + float artistH = (float) artistRenderer.getHeight(false); + artistRenderer.end(); + + metaRenderer.begin(metaScale, false, false); + float metaH = (float) metaRenderer.getHeight(false); + float ownerW = hasSession ? (float) metaRenderer.getWidth(ownerText, false) : 0f; + metaRenderer.end(); + + float contentH = titleH + textGap + artistH + barGap + barH + bottomGap + metaH; + float artSize = Math.max(artBase, contentH + artBoost); + float leftPad = padding + artOverhang; + float bodyH = Math.max(contentH, artSize); + float boxW = leftPad + padding + artSize + gap + contentW; + float boxH = padding * 2f + bodyH; + + p.setSize(boxW, boxH); + + float scale = Math.max(0f, Math.min(1f, p.anim)); + float drawX = p.getX() + (boxW - boxW * scale) * 0.5f; + float drawY = p.getY() + (boxH - boxH * scale) * 0.5f; + + boolean useSyncTheme = p.syncTheme.get(); + boolean blurEnabled = p.isBlurEffect(); + boolean glassEnabled = p.isGlassEffect(); + int baseRgb = glassEnabled ? HudRenderUtil.glassBackgroundRgb() + : (useSyncTheme ? (theme().windowBg() & 0x00FFFFFF) : (p.bg.getArgb() & 0x00FFFFFF)); + int bgAlpha = p.getBgAlpha(); + int bg = (bgAlpha << 24) | baseRgb; + int accent = useSyncTheme ? theme().accent() : (p.accent.getArgb() | 0xFF000000); + int accentSoft = useSyncTheme ? theme().accentSoft() : HudRenderUtil.mixColor(accent, bg, 0.55f); + int stroke = HudRenderUtil.scaleAlpha(accent, 0.75f); + int strokeSoft = HudRenderUtil.scaleAlpha(accentSoft, 0.45f); + int borderColor = HudRenderUtil.mixColor(stroke, strokeSoft, 0.5f); + p.uiAccent = accent; + p.uiAccentSoft = accentSoft; + p.uiText = useSyncTheme ? theme().textPrimary() : (p.text.getArgb() | 0xFF000000); + p.uiTextMuted = useSyncTheme ? theme().textMuted() : HudRenderUtil.mixColor(p.uiText, bg, 0.6f); + + if (glassEnabled) { + MediaPlayerTextHelper.drawGlass(p, drawX, drawY, boxW * scale, boxH * scale, radius * scale, baseScale * scale, p.uiAccentSoft); + } else if (blurEnabled) { + MediaPlayerTextHelper.drawBlur(p, drawX, drawY, boxW * scale, boxH * scale, radius * scale, bg); + } + int bgScaled = glassEnabled ? HudRenderUtil.glassPanelBackground(scale) : HudRenderUtil.scaleAlpha(bg, scale); + boolean useGradient = !glassEnabled && useSyncTheme && p.gradient.get(); + HudRenderUtil.drawHudBackground(renderer, drawX, drawY, boxW * scale, boxH * scale, radius * scale, 1.0f, bgScaled, useGradient); + if (!glassEnabled) { + renderer.roundedRectStroke(drawX, drawY, boxW * scale, boxH * scale, radius * scale, 1.0f, 0.9f * baseScale * scale, HudRenderUtil.scaleAlpha(borderColor, scale)); + } + + float contentY = padding + (bodyH - contentH) * 0.5f; + if (!hasSession) { + MediaPlayerTextHelper.drawNoSession(p, renderer, metaRenderer, drawX, drawY, boxW, boxH, scale, metaScale, mediaAvailable); + return; + } + + float artX = leftPad - (2f * baseScale); + float artY = padding + (bodyH - artSize) * 0.5f; + float textX = leftPad + artSize + gap; + float artistY = contentY + titleH + textGap; + float barY = artistY + artistH + barGap; + float bottomY = barY + barH + bottomGap; + + MediaPlayerTextHelper.drawArtwork(p, renderer, snapshot, drawX, drawY, artX, artY, artSize, radius, baseScale, scale); + MediaPlayerTextHelper.drawTexts(p, renderer, titleRenderer, artistRenderer, metaRenderer, iconRenderer, titleText, artistText, timeText, ownerText, ownerW, + drawX, drawY, textX, contentY, artistY, barY, bottomY, contentW, titleW, titleH, titleScale, artistScale, metaScale, iconScale, + barH, baseScale, scale, snapshot, btnSizeBase, buttonGap, metaH, showShuffle, showRepeat); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/mediaplayer/MediaPlayerTextHelper.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/mediaplayer/MediaPlayerTextHelper.java new file mode 100644 index 0000000..3a22f7f --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/combat/mediaplayer/MediaPlayerTextHelper.java @@ -0,0 +1,164 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.combat; + +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; +import dev.loki.lovisual.render.engine.color.RenderColor; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.util.media.session.MediaSessionService; +import net.minecraft.client.resources.language.I18n; +import net.minecraft.resources.Identifier; +import net.minecraft.util.Util; + +final class MediaPlayerTextHelper { + + private MediaPlayerTextHelper() {} + + static void drawScrollingTitle(MediaPlayer p, TextRenderer renderer, String text, float x, float y, float fullWidth, float viewWidth, float scale, float baseScale, float alpha) { + float speed = MediaPlayerConstants.BASE_TITLE_SCROLL_SPEED * baseScale; + float gap = MediaPlayerConstants.BASE_TITLE_SCROLL_GAP * baseScale; + if (speed <= 0f) return; + float cycleDistance = fullWidth + gap; + float cycleTime = MediaPlayerConstants.TITLE_SCROLL_PAUSE_SEC + (cycleDistance / speed); + if (cycleTime <= 0f) return; + float now = Util.getMillis() / 1000f; + float t = now % cycleTime; + float offset = 0f; + if (t > MediaPlayerConstants.TITLE_SCROLL_PAUSE_SEC) { + offset = -(t - MediaPlayerConstants.TITLE_SCROLL_PAUSE_SEC) * speed; + } + int color = HudRenderUtil.scaleAlpha(p.uiText, alpha); + RenderColor renderColor = new RenderColor(color); + float fade = Math.min(viewWidth * 0.25f, MediaPlayerConstants.BASE_TITLE_FADE * baseScale); + renderer.begin(scale, false, false); + try { + renderer.renderHorizontalFadeClipped(text, x + offset, y, renderColor, x, x + viewWidth, fade, fade, false); + if (cycleDistance > viewWidth * 0.5f) { + renderer.renderHorizontalFadeClipped(text, x + offset + cycleDistance, y, renderColor, x, x + viewWidth, fade, fade, false); + } + } finally { + renderer.end(); + } + } + + static float getTextHeight(TextRenderer renderer, float scale) { + renderer.begin(scale, false, false); + try { + return (float) renderer.getHeight(false); + } finally { + renderer.end(); + } + } + + static String trimText(TextRenderer tr, String text, float maxWidth, float scale) { + if (text == null || text.isEmpty()) return ""; + tr.begin(scale, false, false); + try { + if (tr.getWidth(text, false) <= maxWidth) return text; + String value = text; + while (!value.isEmpty()) { + String candidate = value + "..."; + if (tr.getWidth(candidate, false) <= maxWidth) return candidate; + value = value.substring(0, value.length() - 1); + } + return ""; + } finally { + tr.end(); + } + } + + static void drawNoSession(MediaPlayer p, Renderer2D renderer, TextRenderer metaRenderer, float drawX, float drawY, float boxW, float boxH, float scale, float metaScale, boolean mediaAvailable) { + String key = mediaAvailable ? "hud.media_player.available" : "hud.media_player.unavailable"; + String status = I18n.get(key); + metaRenderer.begin(metaScale * scale, false, false); + float statusW = (float) metaRenderer.getWidth(status, false); + float statusH = (float) metaRenderer.getHeight(false); + metaRenderer.end(); + float sx = drawX + (boxW * scale - statusW) * 0.5f; + float sy = drawY + (boxH * scale - statusH) * 0.5f; + metaRenderer.begin(metaScale * scale, false, false); + metaRenderer.render(status, sx, sy, new RenderColor(HudRenderUtil.scaleAlpha(p.uiTextMuted, scale)), false); + metaRenderer.end(); + MediaPlayerControls.updateButtons(p); + } + + static void drawArtwork(MediaPlayer p, Renderer2D renderer, MediaSessionService.Snapshot snapshot, float drawX, float drawY, float artX, float artY, float artSize, float radius, float baseScale, float scale) { + float drawArtX = drawX + artX * scale; + float drawArtY = drawY + artY * scale; + float drawArtSize = artSize * scale; + float artRadius = Math.min(radius * 1.2f, artSize * 0.22f) * scale; + Identifier tex = snapshot.artworkTexture(); + if (tex != null) { + Renderer2D.TEXTURE.roundedTexRect(drawArtX, drawArtY, drawArtSize, drawArtSize, artRadius, 1.0f, HudRenderUtil.scaleAlpha(0xFFFFFFFF, scale), tex); + } else { + renderer.roundedRect(drawArtX, drawArtY, drawArtSize, drawArtSize, artRadius, 1.0f, HudRenderUtil.scaleAlpha(0x44111111, scale)); + renderer.roundedRectStroke(drawArtX, drawArtY, drawArtSize, drawArtSize, artRadius, 1.0f, 0.9f * baseScale * scale, HudRenderUtil.scaleAlpha(p.uiAccentSoft, scale * 0.9f)); + } + } + + static void drawBlur(MediaPlayer p, float x, float y, float w, float h, float radius, int tintRgb) { + float quality = p.hud.getBlurRadius(); + float alpha = p.blurAlpha.get() / 255f; + Renderer2D.COLOR.blurRect(x, y, w, h, radius, quality, 1.0f, alpha, 0xFFFFFF); + } + + static void drawGlass(MediaPlayer p, float x, float y, float w, float h, float radius, float scale, int accentSoft) { + float alpha = p.blurAlpha.get() / 255f; + HudRenderUtil.drawLiquidGlass(x, y, w, h, radius, scale, true, alpha); + } + + static void drawTexts(MediaPlayer p, Renderer2D renderer, TextRenderer titleRenderer, TextRenderer artistRenderer, TextRenderer metaRenderer, TextRenderer iconRenderer, + String titleText, String artistText, String timeText, String ownerText, float ownerW, + float drawX, float drawY, float textX, float contentY, float artistY, float barY, float bottomY, + float contentW, float titleW, float titleH, float titleScale, float artistScale, float metaScale, float iconScale, + float barH, float baseScale, float scale, MediaSessionService.Snapshot snapshot, float btnSizeBase, float buttonGap, float metaH, boolean showShuffle, boolean showRepeat) { + float titleRenderX = drawX + textX * scale; + float titleRenderY = drawY + contentY * scale; + float titleRenderW = contentW * scale; + boolean titleScroll = titleW > contentW * 1.01f; + if (titleScroll) { + drawScrollingTitle(p, titleRenderer, titleText, titleRenderX, titleRenderY, titleW * scale, titleRenderW, titleScale * scale, baseScale, scale); + } else { + titleRenderer.begin(titleScale * scale, false, false); + titleRenderer.render(titleText, titleRenderX, titleRenderY, new RenderColor(HudRenderUtil.scaleAlpha(p.uiText, scale)), false); + titleRenderer.end(); + } + artistRenderer.begin(artistScale * scale, false, false); + artistRenderer.render(artistText, drawX + textX * scale, drawY + artistY * scale, new RenderColor(HudRenderUtil.scaleAlpha(p.uiTextMuted, scale)), false); + artistRenderer.end(); + float barX = drawX + textX * scale; + float barW = contentW * scale; + float barRadius = Math.max(1f, barH * 0.5f) * scale; + int barBg = HudRenderUtil.scaleAlpha(0x55000000, scale); + renderer.roundedRect(barX, drawY + barY * scale, barW, barH * scale, barRadius, 1.0f, barBg); + long predictedPos = snapshot.predictedPositionSeconds(); + long predictedDur = snapshot.durationSeconds(); + float progress = predictedDur > 0 ? Math.min(1.0f, Math.max(0.0f, (float) predictedPos / (float) predictedDur)) : 0f; + int barFill = HudRenderUtil.scaleAlpha(p.uiAccent, scale); + renderer.roundedRect(barX, drawY + barY * scale, barW * progress, barH * scale, barRadius, 1.0f, barFill); + metaRenderer.begin(metaScale * scale, false, false); + metaRenderer.render(timeText, drawX + textX * scale, drawY + bottomY * scale, new RenderColor(HudRenderUtil.scaleAlpha(p.uiTextMuted, scale)), false); + float ownerX = drawX + (textX + contentW - ownerW) * scale; + metaRenderer.render(ownerText, ownerX, drawY + bottomY * scale, new RenderColor(HudRenderUtil.scaleAlpha(p.uiTextMuted, scale)), false); + metaRenderer.end(); + boolean chatOpen = net.minecraft.client.Minecraft.getInstance() != null && dev.loki.lovisual.util.screen.ClientScreen.current() instanceof net.minecraft.client.gui.screens.ChatScreen; + MediaPlayerControls.layoutControls(p, drawX, drawY, textX, contentW, bottomY, metaH, btnSizeBase, buttonGap, baseScale, showShuffle, showRepeat, scale); + float controlIconScale = iconScale * 1.05f; + MediaPlayerControls.updateHover(p, chatOpen, showShuffle, showRepeat); + if (showShuffle) { + MediaPlayerControls.drawControl(renderer, iconRenderer, MediaPlayerConstants.iconString(MediaPlayerConstants.ICON_SHUFFLE), p.shuffleX, p.shuffleY, p.buttonSize, p.shuffleHover, scale, controlIconScale, MediaPlayerControls.shuffleColor(snapshot, scale), p); + } + MediaPlayerControls.drawControl(renderer, iconRenderer, MediaPlayerConstants.iconString(MediaPlayerConstants.ICON_PREV), p.prevX, p.prevY, p.buttonSize, p.prevHover, scale, controlIconScale, HudRenderUtil.scaleAlpha(p.uiText, scale), p); + MediaPlayerControls.drawControl(renderer, iconRenderer, MediaPlayerConstants.iconString(snapshot.isPlaying() ? MediaPlayerConstants.ICON_PAUSE : MediaPlayerConstants.ICON_PLAY), p.playX, p.playY, p.buttonSize, p.playHover, scale, controlIconScale, HudRenderUtil.scaleAlpha(p.uiText, scale), p); + MediaPlayerControls.drawControl(renderer, iconRenderer, MediaPlayerConstants.iconString(MediaPlayerConstants.ICON_NEXT), p.nextX, p.nextY, p.buttonSize, p.nextHover, scale, controlIconScale, HudRenderUtil.scaleAlpha(p.uiText, scale), p); + if (showRepeat) { + MediaPlayerControls.drawRepeatControl(renderer, snapshot, p.repeatX, p.repeatY, p.buttonSize, p.repeatHover, scale, p); + } + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/HudNotifier.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/HudNotifier.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/HudNotifier.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/HudNotifier.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastCardRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastCardRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastCardRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastCardRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastGeometry.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastGeometry.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastGeometry.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/ToastGeometry.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/model/ToastModel.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/model/ToastModel.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/model/ToastModel.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/model/ToastModel.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/model/ToastStyle.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/model/ToastStyle.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/model/ToastStyle.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/hudnotifier/model/ToastStyle.java diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/Coordinates.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/Coordinates.java new file mode 100644 index 0000000..7e52e03 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/Coordinates.java @@ -0,0 +1,174 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.metrics; + +import dev.loki.lovisual.config.values.color.RGBAColorValue; +import dev.loki.lovisual.config.values.color.RGBColorValue; +import dev.loki.lovisual.config.values.mode.EnumValue; +import dev.loki.lovisual.config.values.mode.ModeValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.resources.Identifier; +import dev.loki.lovisual.features.gui.hud.registry.HudElementInfo; +import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; +import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; +import dev.loki.lovisual.features.gui.hud.script.CompactHudStatModel; +import dev.loki.lovisual.features.gui.hud.script.ScriptableHudStatWidget; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; + +//todo Description +@HudElementInfo( + id = "xyz", + displayName = "XYZ", + enabledByDefault = true, + order = 100 +) +public final class Coordinates extends DraggableHudElement implements ScriptableHudStatWidget { + + static final Identifier COORDS_ICON = Identifier.fromNamespaceAndPath("lovisual", "textures/hud/elements/coords.png"); + static final float SCALE_MULT = 0.68f; + static final float BOX_HEIGHT = 20f; + static final float BOX_RADIUS = 5f; + static final float BOX_SOFTNESS = 1.0f; + static final float STROKE_WIDTH = 0.55f; + static final float ICON_SIZE = 11f; + static final float ICON_X = 5f; + static final float DIVIDER_X = 21f; + static final float DIVIDER_Y = 4f; + static final float DIVIDER_WIDTH = 0.6f; + static final float TEXT_LEFT_X = 26f; + static final float SEGMENT_GAP = 8f; + static final float LABEL_VALUE_GAP = 2f; + static final float RIGHT_PAD = 5f; + static final String COLOR_THEME = "Theme"; + static final String COLOR_CUSTOM = "Custom"; + static final String EFFECT_NONE = "None"; + static final String EFFECT_BLUR = "Blur"; + static final String EFFECT_GLASS = "Glass"; + + final HudGlobalConfig hud = HudGlobalConfig.get(); + final EnumValue netherMode = + enumSetting("xyz_nether", NetherMode.ON, NetherMode.OFF, NetherMode.ON, NetherMode.ONLY_NETHER); + final NumberValue scale = num("xyz_scale", 2.37, 0.5, 5.0); + final ModeValue colorMode = mode("xyz_color_mode", "color_mode", "Theme", new String[]{COLOR_THEME, COLOR_CUSTOM}); + final RGBColorValue iconColor = visibleWhen(colorNoAlpha("xyz_icon_color", "#FFFFFF"), this::isCustomMode); + final RGBColorValue labelColor = visibleWhen(colorNoAlpha("xyz_label_color", "#9B9B9B"), this::isCustomMode); + final RGBColorValue valueColor = visibleWhen(colorNoAlpha("xyz_value_color", "#FFFFFF"), this::isCustomMode); + final RGBColorValue extraColor = visibleWhen(colorNoAlpha("xyz_extra_color", "#BEBEBE"), this::isCustomMode); + final ModeValue bgEffect = mode("xyz_bg_effect", "bg_effect", "Blur", new String[]{EFFECT_NONE, EFFECT_BLUR, EFFECT_GLASS}); + final RGBAColorValue bg = visibleWhen(color("xyz_bg", "#F7343434"), () -> isCustomMode() && !isGlassEffect()); + final RGBAColorValue bg2 = visibleWhen(color("xyz_bg_secondary", "#F7161616"), () -> isCustomMode() && !isGlassEffect()); + final RGBColorValue stroke = visibleWhen(colorNoAlpha("xyz_stroke", "#5A5A5A"), () -> isCustomMode() && !isGlassEffect()); + final NumberValue bgAlpha = visibleWhen(num("xyz_bg_alpha", 184, 0, 255), () -> isThemeMode() && !isGlassEffect()); + final NumberValue blurAlpha = visibleWhen(num("xyz_blur_alpha", 255, 0, 255), this::hasEffect); + final EnumValue labelEffect = + enumSetting("xyz_label_effect", HudTextEffects.Effect.FLOW, + HudTextEffects.Effect.NONE, HudTextEffects.Effect.MIX, HudTextEffects.Effect.FLOW, + HudTextEffects.Effect.PULSE, HudTextEffects.Effect.STRIPE); + final NumberValue labelEffectSpeed = + visibleWhen(num("xyz_label_effect_speed", 18, 1, 60), + () -> labelEffect.get() != HudTextEffects.Effect.NONE); + final CompactHudStatModel scriptModel = new CompactHudStatModel("xyz"); + int uiBgPrimary; + int uiBgSecondary; + int uiStroke; + int uiIconColor; + int uiLabelColor; + int uiValueColor; + int uiExtraColor; + float displayXValueWidth = -1f; + float displayYValueWidth = -1f; + float displayZValueWidth = -1f; + float displayExtraWidth = -1f; + + @Override + public void applyDefaultPosition(int screenW, int screenH) { + this.x = 16f; + this.y = screenH - 92f; + } + + @Override + public boolean usesEngineRenderer() { + return true; + } + + @Override + public void renderEngine(Renderer2D renderer, + TextRenderer textRenderer, + GuiGraphicsExtractor ctx, + float tickDelta, + int screenW, + int screenH) { + CoordinatesRenderer.render(this, renderer, textRenderer, ctx, tickDelta, screenW, screenH); + } + + @Override + public boolean supportsWidgetAnchoring() { + return true; + } + + @Override + public CompactHudStatModel compactStatModel() { + return scriptModel; + } + + float hudX() { + return getX(); + } + + float hudY() { + return getY(); + } + + void setHudSize(float w, float h) { + this.width = w; + this.height = h; + } + + boolean shouldShowNether(boolean inNether) { + NetherMode mode = netherMode.get(); + if (mode == NetherMode.ON) return true; + if (mode == NetherMode.ONLY_NETHER) return !inNether; + return false; + } + + boolean isThemeMode() { + return COLOR_THEME.equals(colorMode.get()); + } + + boolean isCustomMode() { + return COLOR_CUSTOM.equals(colorMode.get()); + } + + boolean isGlassEffect() { + return EFFECT_GLASS.equals(bgEffect.get()); + } + + boolean hasEffect() { + return !EFFECT_NONE.equals(bgEffect.get()); + } + + enum NetherMode implements EnumValue.IdProvider { + OFF("Off"), + ON("On"), + ONLY_NETHER("OnlyNether"); + + private final String id; + + NetherMode(String id) { + this.id = id; + } + + @Override + public String id() { + return id; + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/GameTime.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/GameTime.java new file mode 100644 index 0000000..100430a --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/GameTime.java @@ -0,0 +1,150 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.metrics; + +import dev.loki.lovisual.config.values.color.RGBAColorValue; +import dev.loki.lovisual.config.values.color.RGBColorValue; +import dev.loki.lovisual.config.values.mode.EnumValue; +import dev.loki.lovisual.config.values.mode.ModeValue; +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; +import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; +import dev.loki.lovisual.features.gui.hud.registry.HudElementInfo; +import dev.loki.lovisual.features.gui.hud.script.CompactHudStatModel; +import dev.loki.lovisual.features.gui.hud.script.ScriptableHudStatWidget; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; + +@HudElementInfo( + id = "game_time", + displayName = "Game Time", + enabledByDefault = false, + order = 180 +) +public final class GameTime extends DraggableHudElement implements ScriptableHudStatWidget { + + static final float SCALE_MULT = 0.68f; + static final float BOX_HEIGHT = 20f; + static final float BOX_RADIUS = 5f; + static final float BOX_SOFTNESS = 1.0f; + static final float STROKE_WIDTH = 0.55f; + static final float ICON_X = 5f; + static final float DIVIDER_X = 21f; + static final float DIVIDER_Y = 4f; + static final float DIVIDER_WIDTH = 0.6f; + static final float TEXT_LEFT_X = 26f; + static final float RIGHT_PAD = 5f; + static final float ICON_SCALE = 0.92f; + static final String WIDTH_TEMPLATE = "00:00"; + static final String COLOR_THEME = "Theme"; + static final String COLOR_CUSTOM = "Custom"; + static final String EFFECT_NONE = "None"; + static final String EFFECT_BLUR = "Blur"; + static final String EFFECT_GLASS = "Glass"; + static final long SUNRISE_START = 23000L; + static final long SUNRISE_END = 1000L; + static final long SUNSET_START = 12000L; + static final long SUNSET_END = 13000L; + static final int ICON_SUNRISE = 0xEA0E; + static final int ICON_SUN = 0xEA04; + static final int ICON_SUNSET = 0xEA0F; + static final int ICON_RAIN_DAY = 0xEA01; + static final int ICON_RAIN_NIGHT = 0xEA09; + static final int ICON_THUNDER_DAY = 0xEA05; + static final int ICON_THUNDER_NIGHT = 0xEA0C; + static final int ICON_SNOW_DAY = 0xEA03; + static final int ICON_SNOW_NIGHT = 0xEA0B; + static final int ICON_THUNDER_SNOW_DAY = 0xEA02; + static final int ICON_THUNDER_SNOW_NIGHT = 0xEA0A; + + final HudGlobalConfig hud = HudGlobalConfig.get(); + final BooleanValue showWeather = bool("game_time_weather", "weather", true); + final NumberValue scale = num("game_time_scale", 2.37, 0.5, 5.0); + final ModeValue colorMode = mode("game_time_color_mode", "color_mode", "Theme", new String[]{COLOR_THEME, COLOR_CUSTOM}); + final RGBColorValue iconColor = visibleWhen(colorNoAlpha("game_time_icon_color", "#FFFFFF"), this::isCustomMode); + final RGBColorValue valueColor = visibleWhen(colorNoAlpha("game_time_value_color", "#FFFFFF"), this::isCustomMode); + final ModeValue bgEffect = mode("game_time_bg_effect", "bg_effect", "None", new String[]{EFFECT_NONE, EFFECT_BLUR, EFFECT_GLASS}); + final RGBAColorValue bg = visibleWhen(color("game_time_bg", "#EB111318"), () -> isCustomMode() && !isGlassEffect()); + final RGBAColorValue bg2 = visibleWhen(color("game_time_bg_secondary", "#F7161616"), () -> isCustomMode() && !isGlassEffect()); + final RGBColorValue stroke = visibleWhen(colorNoAlpha("game_time_stroke", "#5A5A5A"), () -> isCustomMode() && !isGlassEffect()); + final NumberValue bgAlpha = visibleWhen(num("game_time_bg_alpha", 235, 0, 255), () -> isThemeMode() && !isGlassEffect()); + final NumberValue blurAlpha = visibleWhen(num("game_time_blur_alpha", 140, 0, 255), this::hasEffect); + final EnumValue iconEffect = + enumSetting("game_time_icon_effect", HudTextEffects.Effect.MIX, + HudTextEffects.Effect.NONE, HudTextEffects.Effect.MIX, HudTextEffects.Effect.FLOW, + HudTextEffects.Effect.PULSE, HudTextEffects.Effect.STRIPE); + final NumberValue iconEffectSpeed = + visibleWhen(num("game_time_icon_effect_speed", 18, 1, 60), + () -> iconEffect.get() != HudTextEffects.Effect.NONE); + final CompactHudStatModel scriptModel = new CompactHudStatModel("game_time"); + + int uiBgPrimary; + int uiBgSecondary; + int uiStroke; + int uiIconColor; + int uiValueColor; + float displayTimeWidth = -1f; + + @Override + public void applyDefaultPosition(int screenW, int screenH) { + this.x = 16f; + this.y = 76f; + } + + @Override + public boolean usesEngineRenderer() { + return true; + } + + @Override + public void renderEngine(Renderer2D renderer, + TextRenderer textRenderer, + GuiGraphicsExtractor ctx, + float tickDelta, + int screenW, + int screenH) { + GameTimeRenderer.render(this, renderer, textRenderer, ctx, tickDelta, screenW, screenH); + } + + @Override + public CompactHudStatModel compactStatModel() { + return scriptModel; + } + + float hudX() { + return getX(); + } + + float hudY() { + return getY(); + } + + void setHudSize(float w, float h) { + this.width = w; + this.height = h; + } + + boolean isThemeMode() { + return COLOR_THEME.equals(colorMode.get()); + } + + boolean isCustomMode() { + return COLOR_CUSTOM.equals(colorMode.get()); + } + + boolean isGlassEffect() { + return EFFECT_GLASS.equals(bgEffect.get()); + } + + boolean hasEffect() { + return !EFFECT_NONE.equals(bgEffect.get()); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/SpeedBps.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/SpeedBps.java new file mode 100644 index 0000000..24d0a4b --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/SpeedBps.java @@ -0,0 +1,156 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.metrics; + +import dev.loki.lovisual.config.values.color.RGBAColorValue; +import dev.loki.lovisual.config.values.color.RGBColorValue; +import dev.loki.lovisual.config.values.mode.EnumValue; +import dev.loki.lovisual.config.values.mode.ModeValue; +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.resources.Identifier; +import dev.loki.lovisual.features.gui.hud.registry.HudElementInfo; +import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; +import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; +import dev.loki.lovisual.features.gui.hud.script.CompactHudStatModel; +import dev.loki.lovisual.features.gui.hud.script.ScriptableHudStatWidget; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; + +//todo Description +@HudElementInfo( + id = "speed_bps", + displayName = "Speed BPS", + enabledByDefault = true, + order = 90 +) +public final class SpeedBps extends DraggableHudElement implements ScriptableHudStatWidget { + + static final Identifier SPEED_ICON = Identifier.fromNamespaceAndPath("lovisual", "textures/hud/elements/speedometer.png"); + static final float SCALE_MULT = 0.68f; + static final float BOX_HEIGHT = 20f; + static final float BOX_RADIUS = 5f; + static final float BOX_SOFTNESS = 1.0f; + static final float STROKE_WIDTH = 0.55f; + static final float ICON_SIZE = 11f; + static final float ICON_X = 5f; + static final float DIVIDER_X = 21f; + static final float DIVIDER_Y = 4f; + static final float DIVIDER_WIDTH = 0.6f; + static final float TEXT_LEFT_X = 26f; + static final float RIGHT_PAD = 5f; + static final float KMH_MULT = 3.6f; + static final String COLOR_THEME = "Theme"; + static final String COLOR_CUSTOM = "Custom"; + static final String EFFECT_NONE = "None"; + static final String EFFECT_BLUR = "Blur"; + static final String EFFECT_GLASS = "Glass"; + + static final long SAMPLE_INTERVAL_MS = 75L; + static final int SAMPLE_COUNT = 20; + + final HudGlobalConfig hud = HudGlobalConfig.get(); + final BooleanValue useBps = bool("speed_bps_bps", true); + final BooleanValue average = bool("speed_bps_average", false); + final NumberValue scale = num("speed_bps_scale", 2.37, 0.5, 5.0); + final ModeValue colorMode = mode("speed_bps_color_mode", "color_mode", "Theme", new String[]{COLOR_THEME, COLOR_CUSTOM}); + final RGBColorValue iconColor = visibleWhen(colorNoAlpha("speed_bps_icon_color", "#FFFFFF"), this::isCustomMode); + final RGBColorValue valueColor = visibleWhen(colorNoAlpha("speed_bps_value_color", "#FFFFFF"), this::isCustomMode); + final RGBColorValue metaColor = visibleWhen(colorNoAlpha("speed_bps_meta_color", "#9B9B9B"), this::isCustomMode); + final ModeValue bgEffect = mode("speed_bps_bg_effect", "bg_effect", "Blur", new String[]{EFFECT_NONE, EFFECT_BLUR, EFFECT_GLASS}); + final RGBAColorValue bg = visibleWhen(color("speed_bps_bg", "#F7343434"), () -> isCustomMode() && !isGlassEffect()); + final RGBAColorValue bg2 = visibleWhen(color("speed_bps_bg_secondary", "#F7161616"), () -> isCustomMode() && !isGlassEffect()); + final RGBColorValue stroke = visibleWhen(colorNoAlpha("speed_bps_stroke", "#5A5A5A"), () -> isCustomMode() && !isGlassEffect()); + final NumberValue bgAlpha = visibleWhen(num("speed_bps_bg_alpha", 135, 0, 255), () -> isThemeMode() && !isGlassEffect()); + final NumberValue blurAlpha = visibleWhen(num("speed_bps_blur_alpha", 255, 0, 255), this::hasEffect); + final EnumValue labelEffect = + enumSetting("speed_bps_label_effect", HudTextEffects.Effect.MIX, + HudTextEffects.Effect.NONE, HudTextEffects.Effect.MIX, HudTextEffects.Effect.FLOW, + HudTextEffects.Effect.PULSE, HudTextEffects.Effect.STRIPE); + final NumberValue labelEffectSpeed = + visibleWhen(num("speed_bps_label_effect_speed", 60, 1, 60), + () -> labelEffect.get() != HudTextEffects.Effect.NONE); + final CompactHudStatModel scriptModel = new CompactHudStatModel("speed_bps"); + final float[] samples = new float[SAMPLE_COUNT]; + final StringBuilder valueBuilder = new StringBuilder(16); + int sampleIndex = 0; + int sampleCount = 0; + long lastSampleMs = 0L; + float displayedSpeed = 0f; + int lastTenth = Integer.MIN_VALUE; + String cachedNumber = "0.0"; + float displayValueWidth = -1f; + int uiBgPrimary; + int uiBgSecondary; + int uiStroke; + int uiIconColor; + int uiValueColor; + int uiMetaColor; + + @Override + public void applyDefaultPosition(int screenW, int screenH) { + this.x = 16f; + this.y = screenH - 48f; + } + + @Override + public boolean usesEngineRenderer() { + return true; + } + + @Override + public void renderEngine(Renderer2D renderer, + TextRenderer textRenderer, + GuiGraphicsExtractor ctx, + float tickDelta, + int screenW, + int screenH) { + SpeedBpsRenderer.render(this, renderer, textRenderer, ctx, tickDelta, screenW, screenH); + } + + @Override + public boolean supportsWidgetAnchoring() { + return true; + } + + @Override + public CompactHudStatModel compactStatModel() { + return scriptModel; + } + + boolean isThemeMode() { + return COLOR_THEME.equals(colorMode.get()); + } + + boolean isCustomMode() { + return COLOR_CUSTOM.equals(colorMode.get()); + } + + boolean isGlassEffect() { + return EFFECT_GLASS.equals(bgEffect.get()); + } + + boolean hasEffect() { + return !EFFECT_NONE.equals(bgEffect.get()); + } + + float hudX() { + return getX(); + } + + float hudY() { + return getY(); + } + + void setHudSize(float w, float h) { + this.width = w; + this.height = h; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/SystemTime.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/SystemTime.java new file mode 100644 index 0000000..d9bf596 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/SystemTime.java @@ -0,0 +1,136 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.metrics; + +import dev.loki.lovisual.config.values.color.RGBAColorValue; +import dev.loki.lovisual.config.values.color.RGBColorValue; +import dev.loki.lovisual.config.values.mode.EnumValue; +import dev.loki.lovisual.config.values.mode.ModeValue; +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import dev.loki.lovisual.features.gui.hud.registry.HudElementInfo; +import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; +import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; +import dev.loki.lovisual.features.gui.hud.script.CompactHudStatModel; +import dev.loki.lovisual.features.gui.hud.script.ScriptableHudStatWidget; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; + +@HudElementInfo( + id = "system_time", + displayName = "System Time", + enabledByDefault = false, + order = 170 +) +public final class SystemTime extends DraggableHudElement implements ScriptableHudStatWidget { + + static final float SCALE_MULT = 0.68f; + static final float BOX_HEIGHT = 20f; + static final float BOX_RADIUS = 5f; + static final float BOX_SOFTNESS = 1.0f; + static final float STROKE_WIDTH = 0.55f; + static final float ICON_X = 5f; + static final float DIVIDER_X = 21f; + static final float DIVIDER_Y = 4f; + static final float DIVIDER_WIDTH = 0.6f; + static final float TEXT_LEFT_X = 26f; + static final float RIGHT_PAD = 5f; + static final float ICON_SCALE = 0.88f; + static final String WIDTH_TEMPLATE = "00:00"; + static final String WIDTH_TEMPLATE_SECONDS = "00:00:00"; + static final String COLOR_THEME = "Theme"; + static final String COLOR_CUSTOM = "Custom"; + static final String EFFECT_NONE = "None"; + static final String EFFECT_BLUR = "Blur"; + static final String EFFECT_GLASS = "Glass"; + + final HudGlobalConfig hud = HudGlobalConfig.get(); + final BooleanValue showSeconds = bool("system_time_seconds", false); + final NumberValue scale = num("system_time_scale", 2.37, 0.5, 5.0); + final ModeValue colorMode = mode("system_time_color_mode", "color_mode", "Theme", new String[]{COLOR_THEME, COLOR_CUSTOM}); + final RGBColorValue iconColor = visibleWhen(colorNoAlpha("system_time_icon_color", "#FFFFFF"), this::isCustomMode); + final RGBColorValue valueColor = visibleWhen(colorNoAlpha("system_time_value_color", "#FFFFFF"), this::isCustomMode); + final ModeValue bgEffect = mode("system_time_bg_effect", "bg_effect", "Blur", new String[]{EFFECT_NONE, EFFECT_BLUR, EFFECT_GLASS}); + final RGBAColorValue bg = visibleWhen(color("system_time_bg", "#EB111318"), () -> isCustomMode() && !isGlassEffect()); + final RGBAColorValue bg2 = visibleWhen(color("system_time_bg_secondary", "#F7161616"), () -> isCustomMode() && !isGlassEffect()); + final RGBColorValue stroke = visibleWhen(colorNoAlpha("system_time_stroke", "#5A5A5A"), () -> isCustomMode() && !isGlassEffect()); + final NumberValue bgAlpha = visibleWhen(num("system_time_bg_alpha", 235, 0, 255), () -> isThemeMode() && !isGlassEffect()); + final NumberValue blurAlpha = visibleWhen(num("system_time_blur_alpha", 140, 0, 255), this::hasEffect); + final EnumValue iconEffect = + enumSetting("system_time_icon_effect", HudTextEffects.Effect.NONE, + HudTextEffects.Effect.NONE, HudTextEffects.Effect.MIX, HudTextEffects.Effect.FLOW, + HudTextEffects.Effect.PULSE, HudTextEffects.Effect.STRIPE); + final NumberValue iconEffectSpeed = + visibleWhen(num("system_time_icon_effect_speed", 18, 1, 60), + () -> iconEffect.get() != HudTextEffects.Effect.NONE); + final CompactHudStatModel scriptModel = new CompactHudStatModel("system_time"); + + int uiBgPrimary; + int uiBgSecondary; + int uiStroke; + int uiIconColor; + int uiValueColor; + float displayTimeWidth = -1f; + + @Override + public void applyDefaultPosition(int screenW, int screenH) { + this.x = 16f; + this.y = 48f; + } + + @Override + public boolean usesEngineRenderer() { + return true; + } + + @Override + public void renderEngine(Renderer2D renderer, + TextRenderer textRenderer, + GuiGraphicsExtractor ctx, + float tickDelta, + int screenW, + int screenH) { + SystemTimeRenderer.render(this, renderer, textRenderer, ctx, tickDelta, screenW, screenH); + } + + @Override + public CompactHudStatModel compactStatModel() { + return scriptModel; + } + + float hudX() { + return getX(); + } + + float hudY() { + return getY(); + } + + void setHudSize(float w, float h) { + this.width = w; + this.height = h; + } + + boolean isThemeMode() { + return COLOR_THEME.equals(colorMode.get()); + } + + boolean isCustomMode() { + return COLOR_CUSTOM.equals(colorMode.get()); + } + + boolean isGlassEffect() { + return EFFECT_GLASS.equals(bgEffect.get()); + } + + boolean hasEffect() { + return !EFFECT_NONE.equals(bgEffect.get()); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/coordinates/CoordinatesPalette.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/coordinates/CoordinatesPalette.java new file mode 100644 index 0000000..779d152 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/coordinates/CoordinatesPalette.java @@ -0,0 +1,46 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.metrics; + +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; + +import static dev.loki.lovisual.features.theme.Theme.theme; + +final class CoordinatesPalette { + + private CoordinatesPalette() { + } + + static void update(Coordinates host) { + if (host.isThemeMode()) { + int panelAlpha = host.bgAlpha.get(); + host.uiBgPrimary = HudRenderUtil.setAlpha(theme().windowBg(), panelAlpha); + host.uiBgSecondary = HudRenderUtil.setAlpha( + HudRenderUtil.mixColor(theme().surface(), theme().windowHeader(), 0.35f), + panelAlpha + ); + host.uiStroke = HudRenderUtil.setAlpha( + HudRenderUtil.mixColor(theme().windowStroke(), theme().strokeSoft(), 0.4f), + Math.min(panelAlpha, 190) + ); + host.uiIconColor = theme().textPrimary(); + host.uiLabelColor = theme().textMuted(); + host.uiValueColor = theme().textPrimary(); + host.uiExtraColor = HudRenderUtil.mixColor(theme().textMuted(), theme().textPrimary(), 0.18f); + return; + } + + host.uiBgPrimary = host.bg.getArgb(); + host.uiBgSecondary = host.bg2.getArgb(); + host.uiStroke = host.stroke.getArgb(); + host.uiIconColor = host.iconColor.getArgb(); + host.uiLabelColor = host.labelColor.getArgb(); + host.uiValueColor = host.valueColor.getArgb(); + host.uiExtraColor = host.extraColor.getArgb(); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/coordinates/CoordinatesRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/coordinates/CoordinatesRenderer.java new file mode 100644 index 0000000..2e59503 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/coordinates/CoordinatesRenderer.java @@ -0,0 +1,172 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.metrics; + +import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; +import dev.loki.lovisual.features.gui.hud.script.ScriptedCompactHudStatRenderer; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.math.HudScale; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.font.Fonts; +import dev.loki.lovisual.render.engine.text.font.info.FontInfo; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.util.Mth; +import net.minecraft.util.Util; +import net.minecraft.world.level.Level; + +final class CoordinatesRenderer { + + private CoordinatesRenderer() { + } + + static void render(Coordinates host, + Renderer2D renderer, + TextRenderer textRenderer, + GuiGraphicsExtractor ctx, + float tickDelta, + int screenW, + int screenH) { + boolean preview = DraggableHudElementRegistry.isForceVisible(); + Minecraft mc = Minecraft.getInstance(); + if (mc == null || (mc.player == null && !preview)) { + host.setHudSize(0f, 0f); + host.scriptModel.setVisible(false); + return; + } + if (!preview && !host.isEnabled()) { + host.setHudSize(0f, 0f); + host.scriptModel.setVisible(false); + return; + } + + boolean inNether = mc.level != null && mc.level.dimension() == Level.NETHER; + double rawX = mc.player != null ? mc.player.getX() : 0.0; + double rawY = mc.player != null ? mc.player.getY() : 0.0; + double rawZ = mc.player != null ? mc.player.getZ() : 0.0; + int px = Mth.floor(rawX); + int py = Mth.floor(rawY); + int pz = Mth.floor(rawZ); + boolean showNether = host.shouldShowNether(inNether); + int hx = showNether ? (int) (rawX * (inNether ? 8.0 : 0.125)) : 0; + int hz = showNether ? (int) (rawZ * (inNether ? 8.0 : 0.125)) : 0; + CoordinatesPalette.update(host); + + String xValue = Integer.toString(px); + String yValue = Integer.toString(py); + String zValue = Integer.toString(pz); + String extra = showNether ? "[" + hx + " " + hz + "]" : ""; + + TextRenderer fallback = textRenderer != null ? textRenderer : TextRenderer.get(); + TextRenderer tr = Fonts.renderer("Onest", FontInfo.Type.Regular, fallback); + + float drawScale = HudScale.scale(screenW, screenH) + * (host.hud.getFontSize() / 18f) + * Coordinates.SCALE_MULT + * host.scale.get().floatValue(); + + tr.begin(drawScale, true, false); + float labelXW = (float) tr.getWidth("x", false); + float labelYW = (float) tr.getWidth("y", false); + float labelZW = (float) tr.getWidth("z", false); + float valueXW = (float) tr.getWidth(xValue, false); + float valueYW = (float) tr.getWidth(yValue, false); + float valueZW = (float) tr.getWidth(zValue, false); + float extraW = showNether ? (float) tr.getWidth(extra, false) : 0f; + float textH = (float) tr.getHeight(false); + tr.end(); + + float boxH = Coordinates.BOX_HEIGHT * drawScale; + float iconSize = Coordinates.ICON_SIZE * drawScale; + float cursorX = host.hudX() + Coordinates.TEXT_LEFT_X * drawScale; + float gap = Coordinates.SEGMENT_GAP * drawScale; + float pairGap = Coordinates.LABEL_VALUE_GAP * drawScale; + host.displayXValueWidth = smoothWidth(host.displayXValueWidth, valueXW); + host.displayYValueWidth = smoothWidth(host.displayYValueWidth, valueYW); + host.displayZValueWidth = smoothWidth(host.displayZValueWidth, valueZW); + host.displayExtraWidth = showNether ? smoothWidth(host.displayExtraWidth, extraW) : 0.0f; + float boxW = (cursorX - host.hudX()) + + labelXW + pairGap + host.displayXValueWidth + + gap + + labelYW + pairGap + host.displayYValueWidth + + gap + + labelZW + pairGap + host.displayZValueWidth + + (showNether ? gap + host.displayExtraWidth : 0f) + + Coordinates.RIGHT_PAD * drawScale; + + host.setHudSize(boxW, boxH); + + float baseX = host.hudX(); + float baseY = host.hudY(); + float iconX = baseX + Coordinates.ICON_X * drawScale; + float iconY = baseY + (boxH - iconSize) * 0.5f; + float dividerX = baseX + Coordinates.DIVIDER_X * drawScale; + float dividerY = baseY + Coordinates.DIVIDER_Y * drawScale; + float dividerW = Math.max(0.5f, Coordinates.DIVIDER_WIDTH * drawScale); + float dividerH = Math.max(0.5f, boxH - 2f * Coordinates.DIVIDER_Y * drawScale); + float textY = baseY + (boxH - textH) * 0.5f; + + int iconColor = host.uiIconColor; + boolean useLabelEffect = host.labelEffect.get() != HudTextEffects.Effect.NONE; + float time = useLabelEffect ? (float) (Util.getMillis() / 1000.0) : 0.0f; + if (useLabelEffect) { + iconColor = HudTextEffects.animatedColor(iconColor, host.labelEffect.get(), + host.labelEffectSpeed.get(), time, 0.0f); + } + + float radius = Coordinates.BOX_RADIUS * drawScale; + host.scriptModel.setVisible(true); + host.scriptModel.setRoot(baseX, baseY, boxW, boxH, radius, drawScale); + host.scriptModel.background().set(host.bgEffect.get(), host.isThemeMode(), host.blurAlpha.get() / 255.0f, + host.uiBgPrimary, host.uiBgSecondary, host.uiStroke, Math.max(0.5f, Coordinates.STROKE_WIDTH * drawScale), Coordinates.BOX_SOFTNESS); + host.scriptModel.icon().texture(Coordinates.COORDS_ICON.toString(), iconX - baseX, iconY - baseY, iconSize, iconSize, iconColor); + host.scriptModel.divider().set(dividerX - baseX, dividerY - baseY, dividerW, dividerH, + HudRenderUtil.setAlpha(host.uiLabelColor, 0x52)); + host.scriptModel.value().set(xValue + " " + yValue + " " + zValue, "Onest", drawScale, + cursorX - baseX, textY - baseY, boxW - (cursorX - baseX), host.uiValueColor); + host.scriptModel.unit().hidden(); + if (showNether) { + host.scriptModel.extra().set(extra, "OnestMedium", drawScale, + boxW - Coordinates.RIGHT_PAD * drawScale - host.displayExtraWidth, textY - baseY, host.displayExtraWidth, host.uiExtraColor); + } else { + host.scriptModel.extra().hidden(); + } + host.scriptModel.animation().setLabelEffect(host.labelEffect.get().name(), host.labelEffectSpeed.get(), time) + .setDigitAnimation(false, "", 1.0f, 0.0f); + host.scriptModel.data("x", px) + .data("y", py) + .data("z", pz) + .data("xText", xValue) + .data("yText", yValue) + .data("zText", zValue) + .data("showNether", showNether) + .data("netherX", hx) + .data("netherZ", hz) + .data("netherText", extra) + .data("labelXW", labelXW) + .data("labelYW", labelYW) + .data("labelZW", labelZW) + .data("valueXW", host.displayXValueWidth) + .data("valueYW", host.displayYValueWidth) + .data("valueZW", host.displayZValueWidth) + .data("xyzExtraW", host.displayExtraWidth) + .data("labelColor", dev.loki.lovisual.features.gui.hud.script.CompactHudStatModel.colorString(host.uiLabelColor)) + .data("valueColor", dev.loki.lovisual.features.gui.hud.script.CompactHudStatModel.colorString(host.uiValueColor)); + ScriptedCompactHudStatRenderer.INSTANCE.render(host.scriptModel, renderer, fallback, ctx, tickDelta); + } + + static float smoothWidth(float current, float target) { + if (current < 0.0f) return target; + if (target >= current) return target; + float next = AnimationUtility.approach(current, target, AnimationUtility.deltaTime(), 12.0f); + return AnimationUtility.snap(next, target, 0.25f); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/gametime/GameTimeIcons.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/gametime/GameTimeIcons.java new file mode 100644 index 0000000..aba6cd4 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/gametime/GameTimeIcons.java @@ -0,0 +1,70 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.metrics; + +import dev.loki.lovisual.features.module.modules.visuals.weather.WorldTweaks; +import dev.loki.lovisual.render.engine.text.font.Fonts; +import dev.loki.lovisual.render.engine.text.font.info.FontInfo; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import net.minecraft.client.Minecraft; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.biome.Biome; + +final class GameTimeIcons { + + private GameTimeIcons() { + } + + static String iconString(int codepoint) { + return new String(Character.toChars(codepoint)); + } + + static TextRenderer getWeatherIcons() { + return Fonts.renderer("WeatherIcons", FontInfo.Type.Regular, TextRenderer.get()); + } + + static String formatTime(long time) { + int hours = (int) ((time / 1000 + 6) % 24); + int minutes = (int) ((time % 1000) * 60 / 1000); + return String.format("%02d:%02d", hours, minutes); + } + + static int resolveIcon(GameTime host, long time) { + if (!host.showWeather.get()) return resolveClearIcon(time); + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.level == null || mc.player == null) return resolveClearIcon(time); + + boolean thunder = WorldTweaks.isServerThundering(mc.level); + boolean rain = WorldTweaks.isServerRaining(mc.level); + if (!thunder && !rain) return resolveClearIcon(time); + + boolean day = time < 12000L; + boolean snow = isSnowingAtPlayer(); + if (thunder) { + return snow ? (day ? GameTime.ICON_THUNDER_SNOW_DAY : GameTime.ICON_THUNDER_SNOW_NIGHT) + : (day ? GameTime.ICON_THUNDER_DAY : GameTime.ICON_THUNDER_NIGHT); + } + if (snow) return day ? GameTime.ICON_SNOW_DAY : GameTime.ICON_SNOW_NIGHT; + return day ? GameTime.ICON_RAIN_DAY : GameTime.ICON_RAIN_NIGHT; + } + + static int resolveClearIcon(long time) { + if (time >= GameTime.SUNRISE_START || time < GameTime.SUNRISE_END) return GameTime.ICON_SUNRISE; + if (time >= GameTime.SUNSET_START && time < GameTime.SUNSET_END) return GameTime.ICON_SUNSET; + return GameTime.ICON_SUN; + } + + static boolean isSnowingAtPlayer() { + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.level == null || mc.player == null) return false; + BlockPos pos = mc.player.blockPosition(); + Biome.Precipitation precipitation = mc.level.getBiome(pos).value() + .getPrecipitationAt(pos, mc.level.getSeaLevel()); + return precipitation == Biome.Precipitation.SNOW; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/gametime/GameTimePalette.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/gametime/GameTimePalette.java new file mode 100644 index 0000000..f4db563 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/gametime/GameTimePalette.java @@ -0,0 +1,42 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.metrics; + +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; + +import static dev.loki.lovisual.features.theme.Theme.theme; + +final class GameTimePalette { + + private GameTimePalette() { + } + + static void update(GameTime host) { + if (host.isThemeMode()) { + int panelAlpha = host.bgAlpha.get(); + host.uiBgPrimary = HudRenderUtil.setAlpha(theme().windowBg(), panelAlpha); + host.uiBgSecondary = HudRenderUtil.setAlpha( + HudRenderUtil.mixColor(theme().surface(), theme().windowHeader(), 0.35f), + panelAlpha + ); + host.uiStroke = HudRenderUtil.setAlpha( + HudRenderUtil.mixColor(theme().windowStroke(), theme().strokeSoft(), 0.4f), + Math.min(panelAlpha, 190) + ); + host.uiIconColor = theme().textPrimary(); + host.uiValueColor = theme().textPrimary(); + return; + } + + host.uiBgPrimary = host.bg.getArgb(); + host.uiBgSecondary = host.bg2.getArgb(); + host.uiStroke = host.stroke.getArgb(); + host.uiIconColor = host.iconColor.getArgb(); + host.uiValueColor = host.valueColor.getArgb(); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/gametime/GameTimeRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/gametime/GameTimeRenderer.java new file mode 100644 index 0000000..81ab4da --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/gametime/GameTimeRenderer.java @@ -0,0 +1,127 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.metrics; + +import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; +import dev.loki.lovisual.features.gui.hud.script.ScriptedCompactHudStatRenderer; +import dev.loki.lovisual.features.module.modules.visuals.weather.WorldTweaks; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.math.HudScale; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.font.Fonts; +import dev.loki.lovisual.render.engine.text.font.info.FontInfo; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.util.Util; + +final class GameTimeRenderer { + + private GameTimeRenderer() { + } + + static void render(GameTime host, + Renderer2D renderer, + TextRenderer textRenderer, + GuiGraphicsExtractor ctx, + float tickDelta, + int screenW, + int screenH) { + boolean preview = DraggableHudElementRegistry.isForceVisible(); + Minecraft mc = Minecraft.getInstance(); + if ((mc == null || mc.level == null) && !preview) { + host.setHudSize(0f, 0f); + host.scriptModel.setVisible(false); + return; + } + if (!preview && !host.isEnabled()) { + host.setHudSize(0f, 0f); + host.scriptModel.setVisible(false); + return; + } + + long worldTime = mc != null && mc.level != null ? (WorldTweaks.getServerTimeOfDay() % 24000L) : 6000L; + String icon = GameTimeIcons.iconString(GameTimeIcons.resolveIcon(host, worldTime)); + String timeText = GameTimeIcons.formatTime(worldTime); + GameTimePalette.update(host); + + TextRenderer fallback = textRenderer != null ? textRenderer : TextRenderer.get(); + TextRenderer iconRenderer = GameTimeIcons.getWeatherIcons(); + if (iconRenderer == null) iconRenderer = fallback; + TextRenderer valueRenderer = Fonts.renderer("Onest", FontInfo.Type.Regular, fallback); + + float drawScale = HudScale.scale(screenW, screenH) + * (host.hud.getFontSize() / 18f) + * GameTime.SCALE_MULT + * host.scale.get().floatValue(); + + float iconScale = drawScale * GameTime.ICON_SCALE; + iconRenderer.begin(iconScale, true, false); + float iconW = (float) iconRenderer.getWidth(icon, false); + float iconH = (float) iconRenderer.getHeight(false); + iconRenderer.end(); + + valueRenderer.begin(drawScale, true, false); + float timeW = (float) valueRenderer.getWidth(timeText, false); + float timeH = (float) valueRenderer.getHeight(false); + float reservedTimeW = (float) valueRenderer.getWidth(GameTime.WIDTH_TEMPLATE, false); + valueRenderer.end(); + + host.displayTimeWidth = Math.max(smoothWidth(host.displayTimeWidth, timeW), reservedTimeW); + float boxH = GameTime.BOX_HEIGHT * drawScale; + float boxW = GameTime.TEXT_LEFT_X * drawScale + host.displayTimeWidth + GameTime.RIGHT_PAD * drawScale; + host.setHudSize(boxW, boxH); + + float baseX = host.hudX(); + float baseY = host.hudY(); + float iconX = baseX + GameTime.ICON_X * drawScale; + float iconY = baseY + (boxH - iconH) * 0.5f; + float dividerX = baseX + GameTime.DIVIDER_X * drawScale; + float dividerY = baseY + GameTime.DIVIDER_Y * drawScale; + float dividerW = Math.max(0.5f, GameTime.DIVIDER_WIDTH * drawScale); + float dividerH = Math.max(0.5f, boxH - 2f * GameTime.DIVIDER_Y * drawScale); + float textX = baseX + GameTime.TEXT_LEFT_X * drawScale; + float textY = baseY + (boxH - timeH) * 0.5f; + + int resolvedIconColor = host.uiIconColor; + boolean useIconEffect = host.iconEffect.get() != HudTextEffects.Effect.NONE; + float nowSec = useIconEffect ? (float) (Util.getMillis() / 1000.0) : 0.0f; + if (useIconEffect) { + resolvedIconColor = HudTextEffects.animatedColor(resolvedIconColor, host.iconEffect.get(), + host.iconEffectSpeed.get(), nowSec, 0.0f); + } + + float radius = GameTime.BOX_RADIUS * drawScale; + host.scriptModel.setVisible(true); + host.scriptModel.setRoot(baseX, baseY, boxW, boxH, radius, drawScale); + host.scriptModel.background().set(host.bgEffect.get(), host.isThemeMode(), host.blurAlpha.get() / 255.0f, + host.uiBgPrimary, host.uiBgSecondary, host.uiStroke, Math.max(0.5f, GameTime.STROKE_WIDTH * drawScale), GameTime.BOX_SOFTNESS); + host.scriptModel.icon().glyph(icon, "WeatherIcons", iconX - baseX, iconY - baseY, iconW, iconH, iconScale, resolvedIconColor); + host.scriptModel.divider().set(dividerX - baseX, dividerY - baseY, dividerW, dividerH, + HudRenderUtil.setAlpha(host.uiValueColor, 0x52)); + host.scriptModel.value().set(timeText, "Onest", drawScale, textX - baseX, textY - baseY, host.displayTimeWidth, host.uiValueColor); + host.scriptModel.unit().hidden(); + host.scriptModel.extra().hidden(); + host.scriptModel.animation().setLabelEffect(host.iconEffect.get().name(), host.iconEffectSpeed.get(), nowSec) + .setDigitAnimation(false, "", 1.0f, 0.0f); + host.scriptModel.data("worldTime", worldTime) + .data("timeText", timeText) + .data("weatherIcon", icon) + .data("showWeather", host.showWeather.get()); + ScriptedCompactHudStatRenderer.INSTANCE.render(host.scriptModel, renderer, fallback, ctx, tickDelta); + } + + static float smoothWidth(float current, float target) { + if (current < 0.0f) return target; + if (target >= current) return target; + float next = AnimationUtility.approach(current, target, AnimationUtility.deltaTime(), 12.0f); + return AnimationUtility.snap(next, target, 0.25f); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/speedbps/SpeedBpsPalette.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/speedbps/SpeedBpsPalette.java new file mode 100644 index 0000000..5dd6994 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/speedbps/SpeedBpsPalette.java @@ -0,0 +1,44 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.metrics; + +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; + +import static dev.loki.lovisual.features.theme.Theme.theme; + +final class SpeedBpsPalette { + + private SpeedBpsPalette() { + } + + static void update(SpeedBps host) { + if (host.isThemeMode()) { + int panelAlpha = host.bgAlpha.get(); + host.uiBgPrimary = HudRenderUtil.setAlpha(theme().windowBg(), panelAlpha); + host.uiBgSecondary = HudRenderUtil.setAlpha( + HudRenderUtil.mixColor(theme().surface(), theme().windowHeader(), 0.35f), + panelAlpha + ); + host.uiStroke = HudRenderUtil.setAlpha( + HudRenderUtil.mixColor(theme().windowStroke(), theme().strokeSoft(), 0.4f), + Math.min(panelAlpha, 190) + ); + host.uiIconColor = theme().textPrimary(); + host.uiValueColor = theme().textPrimary(); + host.uiMetaColor = theme().textMuted(); + return; + } + + host.uiBgPrimary = host.bg.getArgb(); + host.uiBgSecondary = host.bg2.getArgb(); + host.uiStroke = host.stroke.getArgb(); + host.uiIconColor = host.iconColor.getArgb(); + host.uiValueColor = host.valueColor.getArgb(); + host.uiMetaColor = host.metaColor.getArgb(); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/speedbps/SpeedBpsRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/speedbps/SpeedBpsRenderer.java new file mode 100644 index 0000000..b2e3ea7 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/speedbps/SpeedBpsRenderer.java @@ -0,0 +1,142 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.metrics; + +import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; +import dev.loki.lovisual.features.gui.hud.script.ScriptedCompactHudStatRenderer; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.math.HudScale; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.font.Fonts; +import dev.loki.lovisual.render.engine.text.font.info.FontInfo; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.util.Mth; +import net.minecraft.util.Util; + +final class SpeedBpsRenderer { + + private SpeedBpsRenderer() { + } + + static void render(SpeedBps host, + Renderer2D renderer, + TextRenderer textRenderer, + GuiGraphicsExtractor ctx, + float tickDelta, + int screenW, + int screenH) { + Minecraft mc = Minecraft.getInstance(); + boolean preview = DraggableHudElementRegistry.isForceVisible(); + if (mc == null && !preview) { + host.setHudSize(0f, 0f); + host.scriptModel.setVisible(false); + return; + } + if (!preview && !host.isEnabled()) { + host.setHudSize(0f, 0f); + host.scriptModel.setVisible(false); + return; + } + + float currentBps = preview ? 0f : SpeedBpsSampler.currentBps(); + SpeedBpsSampler.updateSamples(host, currentBps); + + boolean showBps = host.useBps.get(); + boolean useAverage = host.average.get(); + float baseBps = useAverage ? SpeedBpsSampler.averageBps(host, currentBps) : currentBps; + host.displayedSpeed = Mth.lerp(0.35f, host.displayedSpeed, baseBps); + + float valueSpeed = showBps ? host.displayedSpeed : host.displayedSpeed * SpeedBps.KMH_MULT; + String value = SpeedBpsSampler.getSpeedNumber(host, valueSpeed); + String unit = showBps ? "b/s" : "km/h"; + SpeedBpsPalette.update(host); + + TextRenderer fallback = textRenderer != null ? textRenderer : TextRenderer.get(); + TextRenderer valueRenderer = Fonts.renderer("Onest", FontInfo.Type.Regular, fallback); + TextRenderer metaRenderer = Fonts.renderer("OnestMedium", FontInfo.Type.Regular, valueRenderer); + + float drawScale = HudScale.scale(screenW, screenH) + * (host.hud.getFontSize() / 18f) + * SpeedBps.SCALE_MULT + * host.scale.get().floatValue(); + + valueRenderer.begin(drawScale, true, false); + float valueW = (float) valueRenderer.getWidth(value, false); + float valueH = (float) valueRenderer.getHeight(false); + valueRenderer.end(); + + metaRenderer.begin(drawScale, true, false); + float unitW = (float) metaRenderer.getWidth(unit, false); + float unitH = (float) metaRenderer.getHeight(false); + metaRenderer.end(); + + float boxH = SpeedBps.BOX_HEIGHT * drawScale; + float iconX = host.hudX() + SpeedBps.ICON_X * drawScale; + float iconSize = SpeedBps.ICON_SIZE * drawScale; + float unitGap = 2f * drawScale; + host.displayValueWidth = smoothWidth(host.displayValueWidth, valueW); + float valueX = host.hudX() + SpeedBps.TEXT_LEFT_X * drawScale; + float boxW = (valueX - host.hudX()) + host.displayValueWidth + unitGap + unitW + SpeedBps.RIGHT_PAD * drawScale; + host.setHudSize(boxW, boxH); + + float baseX = host.hudX(); + float baseY = host.hudY(); + float iconY = baseY + (boxH - iconSize) * 0.5f; + float dividerX = baseX + SpeedBps.DIVIDER_X * drawScale; + float dividerY = baseY + SpeedBps.DIVIDER_Y * drawScale; + float dividerW = Math.max(0.5f, SpeedBps.DIVIDER_WIDTH * drawScale); + float dividerH = Math.max(0.5f, boxH - 2f * SpeedBps.DIVIDER_Y * drawScale); + float textY = baseY + (boxH - Math.max(valueH, unitH)) * 0.5f; + float unitX = valueX + host.displayValueWidth + unitGap; + + int iconColor = host.uiIconColor; + int dividerColor = host.uiMetaColor; + int valueColor = host.uiValueColor; + int unitColor = host.uiMetaColor; + boolean useLabelEffect = host.labelEffect.get() != HudTextEffects.Effect.NONE; + float time = useLabelEffect ? (float) (Util.getMillis() / 1000.0) : 0.0f; + if (useLabelEffect) { + iconColor = HudTextEffects.animatedColor(iconColor, host.labelEffect.get(), + host.labelEffectSpeed.get(), time, 0.0f); + dividerColor = HudTextEffects.animatedColor(dividerColor, host.labelEffect.get(), + host.labelEffectSpeed.get(), time, 0.15f); + } + + float radius = SpeedBps.BOX_RADIUS * drawScale; + host.scriptModel.setVisible(true); + host.scriptModel.setRoot(baseX, baseY, boxW, boxH, radius, drawScale); + host.scriptModel.background().set(host.bgEffect.get(), host.isThemeMode(), host.blurAlpha.get() / 255.0f, + host.uiBgPrimary, host.uiBgSecondary, host.uiStroke, Math.max(0.5f, SpeedBps.STROKE_WIDTH * drawScale), SpeedBps.BOX_SOFTNESS); + host.scriptModel.icon().texture(SpeedBps.SPEED_ICON.toString(), iconX - baseX, iconY - baseY, iconSize, iconSize, iconColor); + host.scriptModel.divider().set(dividerX - baseX, dividerY - baseY, dividerW, dividerH, + HudRenderUtil.setAlpha(dividerColor, 0x52)); + host.scriptModel.value().set(value, "Onest", drawScale, valueX - baseX, textY - baseY, host.displayValueWidth, valueColor); + host.scriptModel.unit().set(unit, "OnestMedium", drawScale, unitX - baseX, textY - baseY, unitW, unitColor); + host.scriptModel.extra().hidden(); + host.scriptModel.animation().setLabelEffect(host.labelEffect.get().name(), host.labelEffectSpeed.get(), time) + .setDigitAnimation(false, "", 1.0f, 0.0f); + host.scriptModel.data("bps", currentBps) + .data("displayedBps", host.displayedSpeed) + .data("average", useAverage) + .data("useBps", showBps) + .data("valueText", value) + .data("unitText", unit); + ScriptedCompactHudStatRenderer.INSTANCE.render(host.scriptModel, renderer, fallback, ctx, tickDelta); + } + + static float smoothWidth(float current, float target) { + if (current < 0.0f) return target; + if (target >= current) return target; + float next = AnimationUtility.approach(current, target, AnimationUtility.deltaTime(), 12.0f); + return AnimationUtility.snap(next, target, 0.25f); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/speedbps/SpeedBpsSampler.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/speedbps/SpeedBpsSampler.java new file mode 100644 index 0000000..2a92941 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/speedbps/SpeedBpsSampler.java @@ -0,0 +1,62 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.metrics; + +import net.minecraft.client.Minecraft; +import dev.loki.lovisual.util.player.movement.PlayerSpeedUtil; + +final class SpeedBpsSampler { + + private SpeedBpsSampler() { + } + + static float currentBps() { + Minecraft mc = Minecraft.getInstance(); + return PlayerSpeedUtil.getBps(mc != null ? mc.player : null); + } + + static void updateSamples(SpeedBps host, float currentBps) { + long now = System.currentTimeMillis(); + if (now - host.lastSampleMs < SpeedBps.SAMPLE_INTERVAL_MS) return; + host.lastSampleMs = now; + host.samples[host.sampleIndex] = currentBps; + host.sampleIndex = (host.sampleIndex + 1) % SpeedBps.SAMPLE_COUNT; + if (host.sampleCount < SpeedBps.SAMPLE_COUNT) host.sampleCount++; + } + + static float averageBps(SpeedBps host, float fallback) { + if (host.sampleCount <= 0) return fallback; + float sum = 0f; + for (int i = 0; i < host.sampleCount; i++) { + sum += host.samples[i]; + } + return sum / host.sampleCount; + } + + static String getSpeedNumber(SpeedBps host, float speed) { + int tenth = Math.round(speed * 10f); + if (tenth == host.lastTenth) return host.cachedNumber; + host.lastTenth = tenth; + host.valueBuilder.setLength(0); + appendOneDecimal(host.valueBuilder, tenth); + host.cachedNumber = host.valueBuilder.toString(); + return host.cachedNumber; + } + + static void appendOneDecimal(StringBuilder sb, int tenth) { + if (tenth < 0) { + sb.append('-'); + tenth = -tenth; + } + int intPart = tenth / 10; + int frac = tenth - intPart * 10; + sb.append(intPart); + sb.append('.'); + sb.append((char) ('0' + frac)); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/systemtime/SystemTimeIcons.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/systemtime/SystemTimeIcons.java new file mode 100644 index 0000000..e2b7734 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/systemtime/SystemTimeIcons.java @@ -0,0 +1,54 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.metrics; + +import dev.loki.lovisual.render.engine.text.font.Fonts; +import dev.loki.lovisual.render.engine.text.font.info.FontInfo; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; + +import java.time.LocalTime; + +final class SystemTimeIcons { + + private static final String[] CLOCK_ICONS = { + iconString(0xEA10), + iconString(0xEA14), + iconString(0xEA15), + iconString(0xEA16), + iconString(0xEA17), + iconString(0xEA18), + iconString(0xEA19), + iconString(0xEA1A), + iconString(0xEA10), + iconString(0xEA11), + iconString(0xEA12), + iconString(0xEA13) + }; + + private SystemTimeIcons() { + } + + static String iconString(int codepoint) { + return new String(Character.toChars(codepoint)); + } + + static TextRenderer getWeatherIcons() { + return Fonts.renderer("WeatherIcons", FontInfo.Type.Regular, TextRenderer.get()); + } + + static String formatTime(LocalTime time, boolean showSeconds) { + if (showSeconds) { + return String.format("%02d:%02d:%02d", time.getHour(), time.getMinute(), time.getSecond()); + } + return String.format("%02d:%02d", time.getHour(), time.getMinute()); + } + + static String clockIcon(int hour) { + return CLOCK_ICONS[hour % 12]; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/systemtime/SystemTimePalette.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/systemtime/SystemTimePalette.java new file mode 100644 index 0000000..e37e1d7 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/systemtime/SystemTimePalette.java @@ -0,0 +1,42 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.metrics; + +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; + +import static dev.loki.lovisual.features.theme.Theme.theme; + +final class SystemTimePalette { + + private SystemTimePalette() { + } + + static void update(SystemTime host) { + if (host.isThemeMode()) { + int panelAlpha = host.bgAlpha.get(); + host.uiBgPrimary = HudRenderUtil.setAlpha(theme().windowBg(), panelAlpha); + host.uiBgSecondary = HudRenderUtil.setAlpha( + HudRenderUtil.mixColor(theme().surface(), theme().windowHeader(), 0.35f), + panelAlpha + ); + host.uiStroke = HudRenderUtil.setAlpha( + HudRenderUtil.mixColor(theme().windowStroke(), theme().strokeSoft(), 0.4f), + Math.min(panelAlpha, 190) + ); + host.uiIconColor = theme().textPrimary(); + host.uiValueColor = theme().textPrimary(); + return; + } + + host.uiBgPrimary = host.bg.getArgb(); + host.uiBgSecondary = host.bg2.getArgb(); + host.uiStroke = host.stroke.getArgb(); + host.uiIconColor = host.iconColor.getArgb(); + host.uiValueColor = host.valueColor.getArgb(); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/systemtime/SystemTimeRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/systemtime/SystemTimeRenderer.java new file mode 100644 index 0000000..840a81a --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/metrics/systemtime/SystemTimeRenderer.java @@ -0,0 +1,130 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.metrics; + +import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; +import dev.loki.lovisual.features.gui.hud.script.ScriptedCompactHudStatRenderer; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.math.HudScale; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.font.Fonts; +import dev.loki.lovisual.render.engine.text.font.info.FontInfo; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.util.Util; + +import java.time.LocalTime; + +final class SystemTimeRenderer { + + private SystemTimeRenderer() { + } + + static void render(SystemTime host, + Renderer2D renderer, + TextRenderer textRenderer, + GuiGraphicsExtractor ctx, + float tickDelta, + int screenW, + int screenH) { + boolean preview = DraggableHudElementRegistry.isForceVisible(); + if (Minecraft.getInstance() == null && !preview) { + host.setHudSize(0f, 0f); + host.scriptModel.setVisible(false); + return; + } + if (!preview && !host.isEnabled()) { + host.setHudSize(0f, 0f); + host.scriptModel.setVisible(false); + return; + } + + LocalTime now = LocalTime.now(); + String icon = SystemTimeIcons.clockIcon(now.getHour()); + String timeText = SystemTimeIcons.formatTime(now, host.showSeconds.get()); + SystemTimePalette.update(host); + + TextRenderer fallback = textRenderer != null ? textRenderer : TextRenderer.get(); + TextRenderer iconRenderer = SystemTimeIcons.getWeatherIcons(); + if (iconRenderer == null) iconRenderer = fallback; + TextRenderer valueRenderer = Fonts.renderer("Onest", FontInfo.Type.Regular, fallback); + + float drawScale = HudScale.scale(screenW, screenH) + * (host.hud.getFontSize() / 18f) + * SystemTime.SCALE_MULT + * host.scale.get().floatValue(); + + float iconScale = drawScale * SystemTime.ICON_SCALE; + iconRenderer.begin(iconScale, true, false); + float iconW = (float) iconRenderer.getWidth(icon, false); + float iconH = (float) iconRenderer.getHeight(false); + iconRenderer.end(); + + valueRenderer.begin(drawScale, true, false); + float timeW = (float) valueRenderer.getWidth(timeText, false); + float timeH = (float) valueRenderer.getHeight(false); + String widthTemplate = host.showSeconds.get() ? SystemTime.WIDTH_TEMPLATE_SECONDS : SystemTime.WIDTH_TEMPLATE; + float reservedTimeW = (float) valueRenderer.getWidth(widthTemplate, false); + valueRenderer.end(); + + host.displayTimeWidth = Math.max(smoothWidth(host.displayTimeWidth, timeW), reservedTimeW); + float boxH = SystemTime.BOX_HEIGHT * drawScale; + float boxW = SystemTime.TEXT_LEFT_X * drawScale + host.displayTimeWidth + SystemTime.RIGHT_PAD * drawScale; + host.setHudSize(boxW, boxH); + + float baseX = host.hudX(); + float baseY = host.hudY(); + float iconX = baseX + SystemTime.ICON_X * drawScale; + float iconY = baseY + (boxH - iconH) * 0.5f; + float dividerX = baseX + SystemTime.DIVIDER_X * drawScale; + float dividerY = baseY + SystemTime.DIVIDER_Y * drawScale; + float dividerW = Math.max(0.5f, SystemTime.DIVIDER_WIDTH * drawScale); + float dividerH = Math.max(0.5f, boxH - 2f * SystemTime.DIVIDER_Y * drawScale); + float textX = baseX + SystemTime.TEXT_LEFT_X * drawScale; + float textY = baseY + (boxH - timeH) * 0.5f; + + int resolvedIconColor = host.uiIconColor; + boolean useIconEffect = host.iconEffect.get() != HudTextEffects.Effect.NONE; + float nowSec = useIconEffect ? (float) (Util.getMillis() / 1000.0) : 0.0f; + if (useIconEffect) { + resolvedIconColor = HudTextEffects.animatedColor(resolvedIconColor, host.iconEffect.get(), + host.iconEffectSpeed.get(), nowSec, 0.0f); + } + + float radius = SystemTime.BOX_RADIUS * drawScale; + host.scriptModel.setVisible(true); + host.scriptModel.setRoot(baseX, baseY, boxW, boxH, radius, drawScale); + host.scriptModel.background().set(host.bgEffect.get(), host.isThemeMode(), host.blurAlpha.get() / 255.0f, + host.uiBgPrimary, host.uiBgSecondary, host.uiStroke, Math.max(0.5f, SystemTime.STROKE_WIDTH * drawScale), SystemTime.BOX_SOFTNESS); + host.scriptModel.icon().glyph(icon, "WeatherIcons", iconX - baseX, iconY - baseY, iconW, iconH, iconScale, resolvedIconColor); + host.scriptModel.divider().set(dividerX - baseX, dividerY - baseY, dividerW, dividerH, + HudRenderUtil.setAlpha(host.uiValueColor, 0x52)); + host.scriptModel.value().set(timeText, "Onest", drawScale, textX - baseX, textY - baseY, host.displayTimeWidth, host.uiValueColor); + host.scriptModel.unit().hidden(); + host.scriptModel.extra().hidden(); + host.scriptModel.animation().setLabelEffect(host.iconEffect.get().name(), host.iconEffectSpeed.get(), nowSec) + .setDigitAnimation(false, "", 1.0f, 0.0f); + host.scriptModel.data("timeText", timeText) + .data("clockIcon", icon) + .data("showSeconds", host.showSeconds.get()) + .data("hour", now.getHour()) + .data("minute", now.getMinute()) + .data("second", now.getSecond()); + ScriptedCompactHudStatRenderer.INSTANCE.render(host.scriptModel, renderer, fallback, ctx, tickDelta); + } + + static float smoothWidth(float current, float target) { + if (current < 0.0f) return target; + if (target >= current) return target; + float next = AnimationUtility.approach(current, target, AnimationUtility.deltaTime(), 12.0f); + return AnimationUtility.snap(next, target, 0.25f); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/Keybinds.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/Keybinds.java new file mode 100644 index 0000000..9b2c160 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/Keybinds.java @@ -0,0 +1,157 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.panels; + +import dev.loki.lovisual.config.definition.SettingDef; +import dev.loki.lovisual.config.values.color.RGBAColorValue; +import dev.loki.lovisual.config.values.color.RGBColorValue; +import dev.loki.lovisual.config.values.mode.ModeValue; +import dev.loki.lovisual.config.values.primitive.BooleanMapValue; +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; +import dev.loki.lovisual.features.gui.hud.draggable.impl.HudPanelLayoutModes; +import dev.loki.lovisual.features.gui.hud.draggable.impl.scripted.ScriptedListHudPanel; +import dev.loki.lovisual.features.gui.hud.registry.HudElementRegister; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import net.minecraft.client.gui.GuiGraphicsExtractor; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@HudElementRegister(order = 45) +public final class Keybinds extends DraggableHudElement { + + { + defaultLinkedLayout(456.6228f, 248.0f, "potions", "RIGHT", 0.0f, 0.0f); + } + + static final float HEADER_HEIGHT = 15.5f; + static final float CONTENT_START_Y = 25.0f; + static final float ROW_STEP = 11.0f; + static final float MIN_WIDTH = 102.0f; + static final float HEADER_ICON_SCALE = 1.18f; + static final float TITLE_TEXT_X = 22.0f; + static final float COUNT_LABEL_OFFSET = 22.0f; + static final String COLOR_THEME = "Theme"; + static final String COLOR_CUSTOM = "Custom"; + static final String EFFECT_NONE = "None"; + static final String EFFECT_BLUR = "Blur"; + static final String MODE_PRESS = "Press"; + static final String MODE_HOLD = "Hold"; + + final HudGlobalConfig hud = HudGlobalConfig.get(); + final ScriptedListHudPanel scriptedPanel = new ScriptedListHudPanel(); + final NumberValue scaleValue = new NumberValue<>("keybinds_scale", 1.0, HudPanelLayoutModes.SCALE_MIN, HudPanelLayoutModes.SCALE_MAX); + final ModeValue layoutMode = new ModeValue("keybinds_layout", "Unified Divider", HudPanelLayoutModes.SPLIT_HEADER, HudPanelLayoutModes.UNIFIED_DIVIDER); + final ModeValue colorMode = new ModeValue("keybinds_color_mode", "Theme", COLOR_THEME, COLOR_CUSTOM); + final ModeValue bgEffect = new ModeValue("keybinds_bg_effect", "None", EFFECT_NONE, EFFECT_BLUR); + final RGBAColorValue bg = new RGBAColorValue("keybinds_bg", "#F7343434"); + final RGBAColorValue bg2 = new RGBAColorValue("keybinds_bg_secondary", "#F7161616"); + final NumberValue bgAlpha = new NumberValue<>("keybinds_bg_alpha", 225, 0, 255); + final RGBColorValue stroke = new RGBColorValue("keybinds_stroke", "#5A5A5A"); + final RGBColorValue text = new RGBColorValue("keybinds_text", "#FFFFFF"); + final RGBColorValue muted = new RGBColorValue("keybinds_muted", "#A5A5A5"); + final NumberValue blurAlpha = new NumberValue<>("keybinds_blur_alpha", 140, 0, 255); + final BooleanValue localActions = new BooleanValue("keybinds_local_actions", true); + final BooleanMapValue localActionModes = new BooleanMapValue("keybinds_local_action_modes", new LinkedHashMap<>() {{ + put(MODE_PRESS, true); + put(MODE_HOLD, true); + }}); + final BooleanValue headerIconPulse = new BooleanValue("keybinds_header_icon_pulse", true); + final NumberValue headerIconPulseSpeed = new NumberValue<>("keybinds_header_icon_pulse_speed", 18, 1, 60); + final NumberValue headerIconPulseIntensity = new NumberValue<>("keybinds_header_icon_pulse_intensity", 100, 0, 100); + + final Map entryAnim = new LinkedHashMap<>(); + final Map lastEntries = new LinkedHashMap<>(); + float displayWidth = -1.0f; + float displayHeight = -1.0f; + float visibilityAnim = 0.0f; + int uiHeaderLeft; + int uiHeaderRight; + int uiBodyLeft; + int uiBodyRight; + int uiOutline; + int uiText; + int uiMuted; + int uiCounter; + int uiTitleText; + int uiDivider; + int uiBlurTint; + + public Keybinds() { + super("keybinds", "Keybinds", true); + } + + @Override + protected void defineSettings(List defs) { + defs.add(SettingDef.number(scaleValue)); + defs.add(SettingDef.mode(layoutMode)); + defs.add(SettingDef.mode(colorMode)); + defs.add(SettingDef.color(bg).visibleWhen(this::isCustomMode)); + defs.add(SettingDef.color(bg2).visibleWhen(this::isCustomMode)); + defs.add(SettingDef.colorNoAlpha(stroke).visibleWhen(this::isCustomMode)); + defs.add(SettingDef.number(bgAlpha).visibleWhen(this::isThemeMode)); + defs.add(SettingDef.colorNoAlpha(text).visibleWhen(this::isCustomMode)); + defs.add(SettingDef.colorNoAlpha(muted).visibleWhen(this::isCustomMode)); + defs.add(SettingDef.mode(bgEffect)); + defs.add(SettingDef.number(blurAlpha).visibleWhen(this::hasEffect)); + defs.add(SettingDef.bool(localActions)); + defs.add(SettingDef.group("keybinds_local_action_modes", localActionModes).visibleWhen(localActions::get)); + defs.add(SettingDef.bool(headerIconPulse)); + defs.add(SettingDef.number(headerIconPulseSpeed).visibleWhen(headerIconPulse::get)); + defs.add(SettingDef.number(headerIconPulseIntensity).visibleWhen(headerIconPulse::get)); + } + + @Override + public void applyDefaultPosition(int screenW, int screenH) { + this.x = screenW - 128.0f; + this.y = 80.0f; + } + + @Override + public boolean usesEngineRenderer() { + return true; + } + + @Override + public void renderEngine(Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta, int screenW, int screenH) { + KeybindsRenderer.render(this, renderer, textRenderer, ctx, tickDelta, screenW, screenH); + } + + @Override + public boolean isDraggable() { + return super.isDraggable(); + } + + boolean isThemeMode() { + return KeybindsPalette.isThemeMode(this); + } + + boolean isCustomMode() { + return KeybindsPalette.isCustomMode(this); + } + + boolean hasEffect() { + return KeybindsPalette.hasEffect(this); + } + + void setSize(float w, float h) { + setBounds(getX(), getY(), w, h); + } + + void setEmpty() { + setBounds(getX(), getY(), 0.0f, 0.0f); + } + + record Row(String key, String name, String bindText, String icon, boolean local) {} + record AnimatedRow(Row row, float anim) {} +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/ModuleList.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/ModuleList.java new file mode 100644 index 0000000..034fa92 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/ModuleList.java @@ -0,0 +1,181 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.panels; + +import net.minecraft.client.gui.GuiGraphicsExtractor; +import dev.loki.lovisual.config.i18n.ConfigValueAliasProvider; +import dev.loki.lovisual.config.definition.SettingDef; +import dev.loki.lovisual.config.values.mode.EnumValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import dev.loki.lovisual.config.values.color.RGBAColorValue; +import dev.loki.lovisual.config.values.color.RGBColorValue; +import dev.loki.lovisual.features.gui.hud.registry.HudElementRegister; +import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; +import dev.loki.lovisual.features.module.core.Module; +import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; + +import java.util.ArrayList; +import java.util.List; + +@HudElementRegister(order = 10) +public final class ModuleList extends DraggableHudElement implements ConfigValueAliasProvider { + + static final float BASE_LINE_H = 9f; + static final float BASE_TEXT_Y = 3f; + static final float BASE_TEXT_X = 3f; + static final float BASE_TEXT_X_REV = 2f; + static final float BASE_STRING_PAD = 3f; + static final float BASE_RECT_EXTRA = 1f; + static final float BASE_RADIUS = 2f; + static final float BASE_SOFTNESS = 0.55f; + + final HudGlobalConfig hud = HudGlobalConfig.get(); + final NumberValue scaleValue = + new NumberValue<>("module_list_scale", 3.67, 0.5, 5.0); + final EnumValue colorMode = + new EnumValue<>("module_list_color_mode", ColorMode.CUSTOM, ColorMode.THEME, ColorMode.CUSTOM); + final EnumValue bgEffect = + new EnumValue<>("module_list_bg_effect", BgEffect.BLUR, BgEffect.NONE, BgEffect.BLUR, BgEffect.GLASS); + final NumberValue bgAlpha = + new NumberValue<>("module_list_bg_alpha", 226, 0, 255); + final NumberValue blurAlpha = + new NumberValue<>("module_list_blur_alpha", 255, 0, 255); + final RGBAColorValue bgTopColor = + new RGBAColorValue("module_list_bg", "#FF444242"); + final RGBAColorValue bgBottomColor = + new RGBAColorValue("module_list_bg_secondary", "#22000000"); + final RGBColorValue textTopColor = + new RGBColorValue("module_list_text", "RNB#E87171"); + final RGBColorValue textBottomColor = + new RGBColorValue("module_list_text_secondary", "#93118C"); + final EnumValue textGradientMode = + new EnumValue<>("module_list_text_gradient_mode", + AnimatedRenderColors.Mode.RAINBOW, + AnimatedRenderColors.Mode.STATIC, + AnimatedRenderColors.Mode.RAINBOW, + AnimatedRenderColors.Mode.LIGHT_RAINBOW, + AnimatedRenderColors.Mode.SKY, + AnimatedRenderColors.Mode.FADE, + AnimatedRenderColors.Mode.DOUBLE_COLOR, + AnimatedRenderColors.Mode.ANALOGOUS, + AnimatedRenderColors.Mode.THEME); + final NumberValue textGradientSpeed = + new NumberValue<>("module_list_text_gradient_speed", 18, 2, 54); + final EnumValue textEffect = + new EnumValue<>("module_list_text_effect", TextEffect.NONE, TextEffect.NONE, TextEffect.SHIMMER); + final NumberValue textEffectSpeed = + new NumberValue<>("module_list_text_effect_speed", 30, 1, 60); + + final List rows = new ArrayList<>(); + final List enabledModules = new ArrayList<>(); + float renderX; + float renderY; + int cachedModulesHash; + int cachedModulesCount; + float cachedTextScale = -1f; + + public ModuleList() { + super("module_list", "ModuleList", false); + } + + @Override + public List getLegacyValueNames(String currentValueName) { + if (currentValueName == null) return List.of(); + if (currentValueName.startsWith("module_list_")) { + return List.of("active_modules_" + currentValueName.substring("module_list_".length())); + } + return List.of(); + } + + @Override + protected void defineSettings(List defs) { + defs.add(SettingDef.number(scaleValue)); + defs.add(SettingDef.mode(colorMode)); + defs.add(SettingDef.mode(bgEffect)); + defs.add(SettingDef.number(bgAlpha).visibleWhen(this::isThemeMode)); + defs.add(SettingDef.number(blurAlpha).visibleWhen(this::hasEffect)); + defs.add(SettingDef.color(bgTopColor).visibleWhen(this::isCustomMode)); + defs.add(SettingDef.color(bgBottomColor).visibleWhen(this::isCustomMode)); + defs.add(SettingDef.colorNoAlpha(textTopColor).visibleWhen(this::isCustomMode)); + defs.add(SettingDef.colorNoAlpha(textBottomColor).visibleWhen(this::isCustomMode)); + defs.add(SettingDef.mode(textGradientMode)); + defs.add(SettingDef.number(textGradientSpeed).visibleWhen(this::hasAnimatedTextGradient)); + defs.add(SettingDef.mode(textEffect)); + defs.add(SettingDef.number(textEffectSpeed).visibleWhen(() -> textEffect.get() != TextEffect.NONE)); + } + + @Override + public void applyDefaultPosition(int screenW, int screenH) { + this.x = screenW - 80f; + this.y = 20f; + } + + @Override + public boolean usesEngineRenderer() { + return true; + } + + @Override + public void renderEngine(Renderer2D renderer, + TextRenderer textRenderer, + GuiGraphicsExtractor ctx, + float tickDelta, + int screenW, + int screenH) { + ModuleListRenderer.render(this, renderer, textRenderer, ctx, tickDelta, screenW, screenH); + } + + @Override + public boolean contains(float mx, float my) { + return mx >= renderX && mx <= renderX + width + && my >= renderY && my <= renderY + height; + } + + boolean isThemeMode() { + return ModuleListPalette.isThemeMode(this); + } + + boolean isCustomMode() { + return ModuleListPalette.isCustomMode(this); + } + + boolean hasEffect() { + return ModuleListPalette.hasEffect(this); + } + + boolean hasAnimatedTextGradient() { + return ModuleListPalette.hasAnimatedTextGradient(this); + } + + void setSize(float w, float h) { setBounds(getX(), getY(), w, h); } + void setEmpty() { setBounds(getX(), getY(), 0f, 0f); } + void clampX(float maxX) { float nx = getX(); if (nx > maxX) nx = maxX; if (nx < 0f) nx = 0f; moveTo(nx, getY()); } + + enum ColorMode implements EnumValue.IdProvider { THEME("Theme"), CUSTOM("Custom"); private final String id; ColorMode(String id){this.id=id;} @Override public String id(){return id;} } + enum BgEffect implements EnumValue.IdProvider { NONE("None"), BLUR("Blur"), GLASS("Glass"); private final String id; BgEffect(String id){this.id=id;} @Override public String id(){return id;} } + enum TextEffect implements EnumValue.IdProvider { NONE("None"), SHIMMER("Shimmer"); private final String id; TextEffect(String id){this.id=id;} @Override public String id(){return id;} } + + static final class Row { + final Module module; + final String label; + float textWidth; + float x; + float y; + float w; + int fillTop; + int fillBottom; + + Row(Module module, String label) { + this.module = module; + this.label = label; + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/Radar.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/Radar.java new file mode 100644 index 0000000..9f8c8d4 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/Radar.java @@ -0,0 +1,161 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.panels; + +import dev.loki.lovisual.config.definition.SettingDef; +import dev.loki.lovisual.config.values.color.RGBAColorValue; +import dev.loki.lovisual.config.values.color.RGBColorValue; +import dev.loki.lovisual.config.values.mode.EnumValue; +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; +import dev.loki.lovisual.features.gui.hud.registry.HudElementRegister; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; + +import java.util.List; + +/** + * Player radar widget with a rectangular map and text list. + */ +@HudElementRegister(order = 140) +public final class Radar extends DraggableHudElement { + + { + defaultLayout(1666.0f, 17.915222f); + } + + static final float BASE_RADIUS = 6f; + static final float BASE_LINE_W = 1.0f; + static final float BASE_PAD = 3f; + static final float BLIP_SIZE = 4f; + static final float BLIP_MARGIN = 6f; + static final float TEXT_SCALE_MULT = 0.6f; + private static final String FIELD_KEY_PREFIX = "radar_field_"; + private static final Field[] FIELD_OPTIONS = { + Field.NAME, + Field.HP, + Field.DISTANCE, + Field.PING, + Field.NONE + }; + final HudGlobalConfig hud = HudGlobalConfig.get(); + final EnumValue mode = + new EnumValue<>("radar_mode", Mode.RECT, Mode.RECT, Mode.TEXT); + final EnumValue colorMode = + new EnumValue<>("radar_color_mode", ColorMode.SYNC, ColorMode.SYNC, ColorMode.CUSTOM); + final NumberValue size = + new NumberValue<>("radar_size", 217.27, 20.0, 300.0); + final RGBAColorValue bgColor = + new RGBAColorValue("radar_bg_color", "#CC101010"); + final NumberValue bgAlpha = + new NumberValue<>("radar_bg_alpha", 109, 0, 255); + final RGBColorValue textColor = + new RGBColorValue("radar_text_color", "#FFFFFF"); + final EnumValue field1 = fieldSlot(1, Field.NAME); + final EnumValue field2 = fieldSlot(2, Field.HP); + final EnumValue field3 = fieldSlot(3, Field.PING); + final EnumValue field4 = fieldSlot(4, Field.NONE); + final EnumValue field5 = fieldSlot(5, Field.NONE); + final BooleanValue blur = + new BooleanValue("radar_blur", true); + final NumberValue blurAlpha = + new NumberValue<>("radar_blur_alpha", 255, 0, 255); + final BooleanValue gradient = + new BooleanValue("radar_gradient", false); + + public Radar() { + super("radar", "Radar", false); + } + + private static EnumValue fieldSlot(int slot, Field def) { + return new EnumValue<>(FIELD_KEY_PREFIX + slot, def, FIELD_OPTIONS); + } + + @Override + protected void defineSettings(List defs) { + defs.add(SettingDef.mode(mode)); + defs.add(SettingDef.mode(colorMode)); + defs.add(SettingDef.number(size)); + defs.add(SettingDef.color(bgColor) + .visibleWhen(() -> colorMode.get() == ColorMode.CUSTOM)); + defs.add(SettingDef.number(bgAlpha) + .visibleWhen(() -> colorMode.get() == ColorMode.SYNC)); + defs.add(SettingDef.colorNoAlpha(textColor) + .visibleWhen(() -> mode.get() == Mode.TEXT + && colorMode.get() == ColorMode.CUSTOM)); + defs.add(SettingDef.mode(field1) + .visibleWhen(() -> mode.get() == Mode.TEXT)); + defs.add(SettingDef.mode(field2) + .visibleWhen(() -> mode.get() == Mode.TEXT)); + defs.add(SettingDef.mode(field3) + .visibleWhen(() -> mode.get() == Mode.TEXT)); + defs.add(SettingDef.mode(field4) + .visibleWhen(() -> mode.get() == Mode.TEXT)); + defs.add(SettingDef.mode(field5) + .visibleWhen(() -> mode.get() == Mode.TEXT)); + defs.add(SettingDef.bool(blur)); + defs.add(SettingDef.number(blurAlpha).visibleWhen(blur::get)); + defs.add(SettingDef.bool(gradient) + .visibleWhen(() -> colorMode.get() == ColorMode.SYNC)); + } + + @Override + public void applyDefaultPosition(int screenW, int screenH) { + this.x = 120f; + this.y = 120f; + } + + @Override + public boolean usesEngineRenderer() { + return true; + } + + void setSize(float w, float h) { + setBounds(getX(), getY(), w, h); + } + + void setEmpty() { + setBounds(getX(), getY(), 0f, 0f); + } + + @Override + public void renderEngine(Renderer2D renderer, + TextRenderer textRenderer, + GuiGraphicsExtractor ctx, + float tickDelta, + int screenW, + int screenH) { + Minecraft mc = Minecraft.getInstance(); + boolean preview = DraggableHudElementRegistry.isForceVisible(); + if (mc == null && !preview) { + width = 0f; + height = 0f; + return; + } + if (!preview && !isEnabled()) { + width = 0f; + height = 0f; + return; + } + + if (mode.get() == Mode.TEXT) { + RadarTextRenderer.renderText(this, textRenderer, screenW, screenH, preview); + } else { + RadarRectRenderer.renderRect(this, renderer, tickDelta, screenW, screenH); + } + } + + enum Mode implements EnumValue.IdProvider { RECT("Rect"), TEXT("Text"); private final String id; Mode(String id){this.id=id;} @Override public String id(){return id;} } + enum ColorMode implements EnumValue.IdProvider { SYNC("Sync"), CUSTOM("Custom"); private final String id; ColorMode(String id){this.id=id;} @Override public String id(){return id;} } + enum Field implements EnumValue.IdProvider { NAME("Name"), HP("Hp"), DISTANCE("Distance"), PING("Ping"), NONE("None"); private final String id; Field(String id){this.id=id;} @Override public String id(){return id;} } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/Scoreboard.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/Scoreboard.java new file mode 100644 index 0000000..6640867 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/Scoreboard.java @@ -0,0 +1,240 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.panels; + +import dev.loki.lovisual.config.definition.SettingDef; +import dev.loki.lovisual.config.values.color.KeyBindValue; +import dev.loki.lovisual.config.values.color.RGBAColorValue; +import dev.loki.lovisual.config.values.color.RGBColorValue; +import dev.loki.lovisual.config.values.mode.BindMode; +import dev.loki.lovisual.config.values.mode.EnumValue; +import dev.loki.lovisual.config.values.mode.ModeValue; +import dev.loki.lovisual.config.values.primitive.BooleanMapValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; +import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; +import dev.loki.lovisual.features.gui.hud.registry.HudElementRegister; +import dev.loki.lovisual.features.module.lifecycle.Modules; +import dev.loki.lovisual.features.module.modules.visuals.visibility.NoRender; +import dev.loki.lovisual.features.module.phase.HudPhase; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.network.chat.Component; +import net.minecraft.world.scores.Objective; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@HudElementRegister(order = 110) +public final class Scoreboard extends DraggableHudElement { + + { + defaultLayout(1721.69f, 158.0f); + } + + static final float BASE_PAD_X = 8.0f; + static final float BASE_PAD_Y = 8.0f; + static final float BASE_ROW_GAP = 2.0f; + static final float BASE_HEADER_GAP = 8.0f; + static final float BASE_SCORE_GAP = 8.0f; + static final float BASE_FOOTER_GAP = 8.0f; + static final float BASE_RADIUS = 4.0f; + static final float BASE_SOFTNESS = 1.0f; + static final float BASE_STROKE = 0.45f; + static final float BASE_TITLE_SCALE = 0.78f; + static final float BASE_TEXT_SCALE = 0.68f; + static final float BASE_TITLE_LINE_GAP = 1.0f; + + final HudGlobalConfig hud = HudGlobalConfig.get(); + final KeyBindValue toggleBind = new KeyBindValue("toggle_bind", "Э"); + final NumberValue scale = new NumberValue<>("scoreboard_scale", 1.93, 0.5, 5.0); + final ModeValue colorMode = new ModeValue("scoreboard_color_mode", "Theme", "Theme", "Custom"); + final ModeValue bgEffect = new ModeValue("scoreboard_bg_effect", "Blur", "None", "Blur"); + final NumberValue blurAlpha = new NumberValue<>("scoreboard_blur_alpha", 255, 0, 255); + final NumberValue bgAlpha = new NumberValue<>("scoreboard_bg_alpha", 182, 0, 255); + final RGBAColorValue bg = new RGBAColorValue("scoreboard_bg", "#820A0A0A"); + final RGBAColorValue bg2 = new RGBAColorValue("scoreboard_bg_secondary", "#B8141414"); + final RGBColorValue stroke = new RGBColorValue("scoreboard_stroke", "#4A4A4A"); + final RGBColorValue titleColor = new RGBColorValue("scoreboard_title_color", "#FFCC00"); + final RGBColorValue labelColor = new RGBColorValue("scoreboard_label_color", "#FFFFFF"); + final RGBColorValue valueColor = new RGBColorValue("scoreboard_value_color", "#FF4A4A"); + final RGBColorValue footerColor = new RGBColorValue("scoreboard_footer_color", "#FFD44A"); + final EnumValue textEffect = new EnumValue<>("scoreboard_text_effect", + HudTextEffects.Effect.STRIPE, HudTextEffects.Effect.NONE, HudTextEffects.Effect.MIX, + HudTextEffects.Effect.FLOW, HudTextEffects.Effect.PULSE, HudTextEffects.Effect.STRIPE); + final NumberValue textEffectSpeed = new NumberValue<>("scoreboard_text_effect_speed", 18, 1, 60); + final BooleanMapValue textEffectSections = new BooleanMapValue("scoreboard_text_effect_sections", defaultTextEffectSections()); + final List lines = new ArrayList<>(); + boolean toggleArmed = true; + boolean toggleInit = false; + int uiBgPrimary; + int uiBgSecondary; + int uiHeaderBg; + int uiStroke; + int uiTitleColor; + int uiLabelColor; + int uiValueColor; + int uiFooterColor; + float displayLabelWidth = -1.0f; + float displayValueWidth = -1.0f; + float displayFooterWidth = -1.0f; + + public Scoreboard() { + super("scoreboard", "Scoreboard", false); + } + + public static boolean shouldReplaceVanilla() { + return DraggableHudElementRegistry.isEnabled(Scoreboard.class) && !isHiddenByNoRender(); + } + + static float smoothWidth(float current, float target) { + if (current < 0.0f) return target; + if (target >= current) return target; + float next = AnimationUtility.approach(current, target, AnimationUtility.deltaTime(), 12.0f); + return AnimationUtility.snap(next, target, 0.25f); + } + + static boolean isHiddenByNoRender() { + NoRender noRender = Modules.get(NoRender.class); + return noRender != null && noRender.hideScoreboard(); + } + + boolean isHidden() { + return isHiddenByNoRender(); + } + + private static Map defaultTextEffectSections() { + Map sections = new LinkedHashMap<>(); + sections.put("header", true); + sections.put("labels", false); + sections.put("values", false); + sections.put("footer", false); + return sections; + } + + @Override + protected void defineSettings(List defs) { + defs.add(SettingDef.bind(toggleBind, BindMode.PRESS)); + defs.add(SettingDef.number(scale)); + defs.add(SettingDef.mode(colorMode)); + defs.add(SettingDef.mode(bgEffect)); + defs.add(SettingDef.number(bgAlpha).visibleWhen(this::isThemeMode)); + defs.add(SettingDef.number(blurAlpha).visibleWhen(this::hasEffect)); + defs.add(SettingDef.color(bg).visibleWhen(this::isCustomMode)); + defs.add(SettingDef.color(bg2).visibleWhen(this::isCustomMode)); + defs.add(SettingDef.colorNoAlpha(stroke).visibleWhen(this::isCustomMode)); + defs.add(SettingDef.colorNoAlpha(titleColor).visibleWhen(this::isCustomMode)); + defs.add(SettingDef.colorNoAlpha(labelColor).visibleWhen(this::isCustomMode)); + defs.add(SettingDef.colorNoAlpha(valueColor).visibleWhen(this::isCustomMode)); + defs.add(SettingDef.colorNoAlpha(footerColor).visibleWhen(this::isCustomMode)); + defs.add(SettingDef.mode(textEffect)); + defs.add(SettingDef.number(textEffectSpeed).visibleWhen(() -> textEffect.get() != HudTextEffects.Effect.NONE)); + defs.add(SettingDef.group(textEffectSections).visibleWhen(() -> textEffect.get() != HudTextEffects.Effect.NONE)); + } + + @Override + public void applyDefaultPosition(int screenW, int screenH) { + this.x = 16.0f; + this.y = screenH * 0.5f - 96.0f; + } + + @Override + public boolean usesEngineRenderer() { + return true; + } + + @Override + public HudPhase getHudPhase() { + return HudPhase.BEFORE_CHAT; + } + + @Override + public void onTick() { + dev.loki.lovisual.features.gui.hud.draggable.impl.panels.ScoreboardDataHelper.handleTick(this); + } + + private void initToggleState() { + toggleArmed = true; + String combo = toggleBind.get(); + if (combo != null && !combo.isBlank() && !"NONE".equalsIgnoreCase(combo) + && net.minecraft.client.Minecraft.getInstance() != null + && dev.loki.lovisual.util.screen.ClientScreen.current() == null + && dev.loki.lovisual.util.input.KeyManager.isComboHeldAllowScreen(combo)) { + toggleArmed = false; + } + } + + private Objective resolveObjective() { + return dev.loki.lovisual.features.gui.hud.draggable.impl.panels.ScoreboardDataHelper.resolveObjective(); + } + + private Component resolveTitle(Objective objective, boolean preview) { + return dev.loki.lovisual.features.gui.hud.draggable.impl.panels.ScoreboardDataHelper.resolveTitle(objective, preview); + } + + private List resolveLines(Objective objective, boolean preview) { + return dev.loki.lovisual.features.gui.hud.draggable.impl.panels.ScoreboardDataHelper.resolveLines(objective, preview); + } + + private void updatePalette() { + dev.loki.lovisual.features.gui.hud.draggable.impl.panels.ScoreboardRenderHelper.updatePalette(this); + } + + private void drawBackground(Renderer2D renderer, float x, float y, float w, float h, float radius, float s) { + dev.loki.lovisual.features.gui.hud.draggable.impl.panels.ScoreboardRenderHelper.drawBackground(renderer, this, x, y, w, h, radius, s); + } + + private void drawHeader(Renderer2D renderer, float x, float y, float w, float hh, float radius) { + dev.loki.lovisual.features.gui.hud.draggable.impl.panels.ScoreboardRenderHelper.drawHeader(renderer, this, x, y, w, hh, radius); + } + + private void drawBlur(float x, float y, float w, float h, float radius) { + dev.loki.lovisual.features.gui.hud.draggable.impl.panels.ScoreboardRenderHelper.drawBlur(this, x, y, w, h, radius); + } + + @Override + public void renderEngine(Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, + float tickDelta, int screenW, int screenH) { + dev.loki.lovisual.features.gui.hud.draggable.impl.panels.ScoreboardRenderHelper.render(this, renderer, textRenderer, ctx, tickDelta, screenW, screenH); + } + + @Override + public boolean supportsWidgetAnchoring() { + return true; + } + + boolean isThemeMode() { + return "Theme".equals(colorMode.get()); + } + + boolean isCustomMode() { + return "Custom".equals(colorMode.get()); + } + + boolean isBlurEffect() { + return "Blur".equals(bgEffect.get()); + } + + boolean hasEffect() { + return !"None".equals(bgEffect.get()); + } + + void setSize(float w, float h) { + this.width = w; + this.height = h; + } + + record SidebarLine(Component label, Component value, boolean footer) { + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/keybinds/KeybindsAnimator.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/keybinds/KeybindsAnimator.java new file mode 100644 index 0000000..1441364 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/keybinds/KeybindsAnimator.java @@ -0,0 +1,60 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.panels; + +import dev.loki.lovisual.render.engine.animation.AnimationUtility; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +final class KeybindsAnimator { + + private KeybindsAnimator() {} + + static List animateRows(Keybinds host, List rows) { + Map current = new LinkedHashMap<>(); + for (Keybinds.Row row : rows) current.put(row.key(), row); + Map merged = new LinkedHashMap<>(); + merged.putAll(current); + for (Map.Entry e : host.lastEntries.entrySet()) merged.putIfAbsent(e.getKey(), e.getValue()); + host.lastEntries.clear(); + host.lastEntries.putAll(merged); + List toRemove = new ArrayList<>(); + for (Map.Entry e : host.lastEntries.entrySet()) { + String key = e.getKey(); + float target = current.containsKey(key) ? 1.0f : 0.0f; + float anim = host.entryAnim.getOrDefault(key, 0.0f); + anim = AnimationUtility.approach(anim, target, AnimationUtility.deltaTime(), 12.0f); + anim = AnimationUtility.snap(anim, target, 0.02f); + if (anim <= 0.0f && target <= 0.0f) toRemove.add(key); + else host.entryAnim.put(key, anim); + } + for (String key : toRemove) { + host.entryAnim.remove(key); + host.lastEntries.remove(key); + } + List out = new ArrayList<>(); + for (Map.Entry e : host.lastEntries.entrySet()) { + float anim = host.entryAnim.getOrDefault(e.getKey(), 0.0f); + if (anim > 0.01f) out.add(new Keybinds.AnimatedRow(e.getValue(), anim)); + } + return out; + } + + static float totalAnimatedHeight(List rows, float rowStep) { + float out = 0.0f; + for (Keybinds.AnimatedRow r : rows) out += rowStep * r.anim(); + return out; + } + + static int clamp255(int v) { + return Math.max(0, Math.min(255, v)); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/keybinds/KeybindsData.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/keybinds/KeybindsData.java new file mode 100644 index 0000000..00db470 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/keybinds/KeybindsData.java @@ -0,0 +1,160 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.panels; + +import dev.loki.lovisual.config.values.mode.BindMode; +import dev.loki.lovisual.features.gui.clickgui.settings.bind.FunctionBindSetting; +import dev.loki.lovisual.features.gui.hud.draggable.impl.scripted.ScriptedListHudPanel; +import dev.loki.lovisual.features.module.core.Module; +import dev.loki.lovisual.features.module.core.ModuleCategory; +import dev.loki.lovisual.features.module.core.ModuleManager; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; + +final class KeybindsData { + + private KeybindsData() {} + + static String iconFor(ModuleCategory category) { + if (category == null) return "ellipsis"; + return switch (category) { + case COMBAT -> "swords"; + case MOVEMENT -> "accessibility"; + case PLAYER -> "user"; + case VISUALS -> "tree-pine"; + default -> "ellipsis"; + }; + } + + static String cleanActionLabel(FunctionBindSetting setting) { + if (setting == null) return ""; + String explicit = setting.getHudLabel(); + if (explicit != null && !explicit.isBlank()) return explicit; + String actionId = setting.getActionId(); + String display = setting.getDisplayName(); + if (display == null || display.isBlank() || display.equals("Function: " + actionId)) display = actionId; + display = display.replace("Function: ", "").replace('_', ' ').replace('-', ' ').trim(); + if (display.isEmpty()) return ""; + StringBuilder out = new StringBuilder(display.length()); + boolean cap = true; + for (int i = 0; i < display.length(); i++) { + char c = display.charAt(i); + if (Character.isWhitespace(c)) { + if (!out.isEmpty() && out.charAt(out.length() - 1) != ' ') out.append(' '); + cap = true; + continue; + } + out.append(cap ? Character.toUpperCase(c) : c); + cap = false; + } + return out.toString(); + } + + static String formatBind(String raw) { + if (raw == null || raw.isBlank()) return ""; + String[] parts = raw.trim().toUpperCase(Locale.ROOT).split("\\+"); + List out = new ArrayList<>(parts.length); + for (String part : parts) { + String mapped = formatBindPart(part.trim()); + if (!mapped.isEmpty()) out.add(mapped); + } + return String.join("+", out); + } + + static String formatBindPart(String value) { + return switch (value) { + case "LEFT_CONTROL", "RIGHT_CONTROL", "LEFT_CTRL", "RIGHT_CTRL", "CONTROL", "CTRL" -> "CTRL"; + case "LEFT_SHIFT", "RIGHT_SHIFT", "SHIFT" -> "SHIFT"; + case "LEFT_ALT", "RIGHT_ALT", "ALT" -> "ALT"; + case "LEFT_SUPER", "RIGHT_SUPER", "SUPER" -> "WIN"; + case "GRAVE_ACCENT" -> "`"; + case "COMMA" -> ","; + case "PERIOD" -> "."; + case "SEMICOLON" -> ";"; + case "APOSTROPHE" -> "'"; + case "SLASH" -> "/"; + case "BACKSLASH" -> "\\"; + case "MINUS" -> "-"; + case "EQUAL" -> "="; + case "SPACE" -> "SPC"; + case "ENTER" -> "ENT"; + case "ESCAPE" -> "ESC"; + case "DELETE" -> "DEL"; + case "INSERT" -> "INS"; + case "CAPS_LOCK" -> "CAPS"; + case "LEFT_BRACKET" -> "["; + case "RIGHT_BRACKET" -> "]"; + default -> value.startsWith("MOUSE_BUTTON_") ? "M" + value.substring("MOUSE_BUTTON_".length()) : value; + }; + } + + static List collectRows(Keybinds host) { + List rows = new ArrayList<>(); + for (Module module : ModuleManager.getModules()) { + if (module == null || !module.isEnabled()) continue; + String name = module.getDisplayName(); + if (name == null || name.isBlank()) name = module.name(); + String moduleIcon = iconFor(module.getCategory()); + if (module.getKeyBindSetting() != null && module.getKeyBindSetting().getValue() != null) { + String rawBind = module.getKeyBindSetting().getValue().get(); + if (rawBind != null && !rawBind.isBlank() && !"NONE".equalsIgnoreCase(rawBind)) { + rows.add(new Keybinds.Row(module.name(), name, formatBind(rawBind), moduleIcon, false)); + } + } + for (FunctionBindSetting action : module.getActionSettings()) { + if (action == null || !isLocalActionVisible(host, action)) continue; + String rawBind = action.get(); + if (rawBind == null || rawBind.isBlank() || "NONE".equalsIgnoreCase(rawBind)) continue; + String actionLabel = cleanActionLabel(action); + if (actionLabel.isBlank()) actionLabel = action.getActionId(); + String actionIcon = action.getHudIcon(); + if (actionIcon == null || actionIcon.isBlank()) actionIcon = moduleIcon; + rows.add(new Keybinds.Row(module.name() + ":action:" + action.getActionId(), name + ": " + actionLabel, formatBind(rawBind), actionIcon, true)); + } + } + rows.sort((a, b) -> String.CASE_INSENSITIVE_ORDER.compare(a.name(), b.name())); + return rows; + } + + static boolean isLocalActionVisible(Keybinds host, FunctionBindSetting action) { + if (!host.localActions.get()) return false; + BindMode mode = action.getMode(); + if (mode == BindMode.HOLD) return host.localActionModes.get(Keybinds.MODE_HOLD) && action.isHeldForHud(); + return host.localActionModes.get(Keybinds.MODE_PRESS) && action.isHudToggle() && action.isHudToggleActive(); + } + + static List> buildPanelRows(Keybinds host, List animatedRows, boolean showExampleRow) { + List> out = ScriptedListHudPanel.rows(); + if (showExampleRow) { + LinkedHashMap row = ScriptedListHudPanel.row("preview", "svg", "swords", + List.of(ScriptedListHudPanel.textPart("KillAura", KeybindsPalette.withAlpha(host.uiText & 0x00FFFFFF, 255), 0.0f)), + "G", KeybindsPalette.withAlpha(host.uiCounter & 0x00FFFFFF, 255), + KeybindsPalette.withAlpha(host.uiMuted & 0x00FFFFFF, 130), 1.0f); + row.put("iconTint", ScriptedListHudPanel.hex(KeybindsPalette.withAlpha(host.uiCounter & 0x00FFFFFF, 255))); + out.add(row); + return out; + } + for (Keybinds.AnimatedRow ar : animatedRows) { + float anim = ar.anim(); + if (anim <= 0.01f) continue; + Keybinds.Row r = ar.row(); + int rowAlpha = KeybindsAnimator.clamp255(Math.round(255.0f * anim)); + int rowNameColor = r.local() ? KeybindsPalette.withAlpha(host.uiCounter & 0x00FFFFFF, rowAlpha) : KeybindsPalette.withAlpha(host.uiText & 0x00FFFFFF, rowAlpha); + int rowRightColor = KeybindsPalette.withAlpha(host.uiCounter & 0x00FFFFFF, rowAlpha); + int rowDividerColor = r.local() ? KeybindsPalette.withAlpha(host.uiCounter & 0x00FFFFFF, Math.max(42, rowAlpha - 94)) : KeybindsPalette.withAlpha(host.uiMuted & 0x00FFFFFF, Math.max(26, rowAlpha - 120)); + LinkedHashMap row = ScriptedListHudPanel.row(r.key(), "svg", r.icon(), + List.of(ScriptedListHudPanel.textPart(r.name(), rowNameColor, 0.0f)), r.bindText(), rowRightColor, rowDividerColor, anim); + row.put("iconTint", ScriptedListHudPanel.hex(KeybindsPalette.withAlpha(host.uiCounter & 0x00FFFFFF, rowAlpha))); + out.add(row); + } + return out; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/keybinds/KeybindsPalette.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/keybinds/KeybindsPalette.java new file mode 100644 index 0000000..d7fc24b --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/keybinds/KeybindsPalette.java @@ -0,0 +1,66 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.panels; + +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; + +import static dev.loki.lovisual.features.theme.Theme.theme; + +final class KeybindsPalette { + + private KeybindsPalette() {} + + static void updatePalette(Keybinds host) { + if (isThemeMode(host)) { + int alpha = host.bgAlpha.get(); + int window = HudRenderUtil.setAlpha(theme().windowBg(), alpha); + int header = HudRenderUtil.setAlpha(HudRenderUtil.mixColor(theme().windowHeader(), theme().surface(), 0.18f), Math.min(255, alpha + 14)); + int surface = HudRenderUtil.setAlpha(HudRenderUtil.mixColor(theme().surface(), theme().windowBg(), 0.22f), Math.min(255, alpha + 6)); + int deep = HudRenderUtil.setAlpha(HudRenderUtil.mixColor(theme().surface(), theme().windowHeader(), 0.42f), Math.min(255, alpha + 18)); + host.uiHeaderLeft = HudRenderUtil.mixColor(header, window, 0.22f); + host.uiHeaderRight = HudRenderUtil.mixColor(surface, header, 0.52f); + host.uiBodyLeft = HudRenderUtil.mixColor(window, surface, 0.24f); + host.uiBodyRight = HudRenderUtil.mixColor(deep, surface, 0.18f); + host.uiOutline = HudRenderUtil.setAlpha(HudRenderUtil.mixColor(theme().windowStroke(), theme().strokeSoft(), 0.18f), Math.min(255, Math.max(182, alpha + 36))); + host.uiText = theme().textPrimary(); + host.uiTitleText = HudRenderUtil.mixColor(theme().textPrimary(), theme().accent(), 0.14f); + host.uiMuted = HudRenderUtil.mixColor(theme().textMuted(), theme().textPrimary(), 0.16f); + host.uiCounter = HudRenderUtil.mixColor(theme().accent(), theme().textPrimary(), 0.38f); + host.uiDivider = HudRenderUtil.scaleAlpha(host.uiMuted, 0.68f); + host.uiBlurTint = HudRenderUtil.mixColor(HudRenderUtil.mixColor(host.uiHeaderRight, host.uiBodyRight, 0.5f), theme().accent(), 0.14f); + return; + } + host.uiHeaderLeft = host.bg.getArgb(); + host.uiHeaderRight = host.bg2.getArgb(); + host.uiBodyLeft = HudRenderUtil.mixColor(host.bg.getArgb(), host.bg2.getArgb(), 0.15f); + host.uiBodyRight = host.bg2.getArgb(); + host.uiOutline = host.stroke.getArgb(); + host.uiText = host.text.getArgb(); + host.uiTitleText = host.uiText; + host.uiMuted = host.muted.getArgb(); + host.uiCounter = HudRenderUtil.mixColor(host.text.getArgb(), 0xFFE1E1FF, 0.20f); + host.uiDivider = HudRenderUtil.scaleAlpha(host.uiMuted, 0.55f); + host.uiBlurTint = HudRenderUtil.mixColor(host.uiHeaderLeft, host.uiBodyRight, 0.5f); + } + + static int withAlpha(int rgb, int alpha) { + return (KeybindsAnimator.clamp255(alpha) << 24) | (rgb & 0x00FFFFFF); + } + + static boolean isThemeMode(Keybinds host) { + return Keybinds.COLOR_THEME.equals(host.colorMode.get()); + } + + static boolean isCustomMode(Keybinds host) { + return Keybinds.COLOR_CUSTOM.equals(host.colorMode.get()); + } + + static boolean hasEffect(Keybinds host) { + return !Keybinds.EFFECT_NONE.equals(host.bgEffect.get()); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/keybinds/KeybindsRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/keybinds/KeybindsRenderer.java new file mode 100644 index 0000000..6c8c581 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/keybinds/KeybindsRenderer.java @@ -0,0 +1,124 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.panels; + +import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; +import dev.loki.lovisual.features.gui.hud.draggable.impl.HudPanelLayoutModes; +import dev.loki.lovisual.features.gui.hud.draggable.impl.scripted.ScriptedListHudPanel; +import dev.loki.lovisual.render.engine.math.HudScale; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.font.Fonts; +import dev.loki.lovisual.render.engine.text.font.info.FontInfo; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.util.screen.ClientScreen; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.screens.ChatScreen; +import net.minecraft.util.Util; + +import java.util.LinkedHashMap; +import java.util.List; + +final class KeybindsRenderer { + + private KeybindsRenderer() {} + + static void render(Keybinds host, Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta, int screenW, int screenH) { + boolean forceVisible = DraggableHudElementRegistry.isForceVisible(); + Minecraft mc = Minecraft.getInstance(); + if (mc == null || (mc.player == null && !forceVisible)) { + host.setEmpty(); + return; + } + boolean chatPreview = ClientScreen.current() instanceof ChatScreen; + List rows = KeybindsData.collectRows(host); + List animatedRows = KeybindsAnimator.animateRows(host, rows); + boolean showExampleRow = rows.isEmpty() && (forceVisible || chatPreview); + boolean showWidget = !rows.isEmpty() || showExampleRow; + host.visibilityAnim = HudRenderUtil.animateVisibility(host.visibilityAnim, showWidget); + float widgetScale = HudRenderUtil.visibilityScale(host.visibilityAnim); + if (animatedRows.isEmpty() && !showExampleRow && host.visibilityAnim <= 0.0f) { + host.setEmpty(); + return; + } + KeybindsPalette.updatePalette(host); + TextRenderer headerIconRenderer = Fonts.renderer("Icons", FontInfo.Type.Regular, TextRenderer.get()); + TextRenderer headerTextRenderer = Fonts.renderer("Inter", FontInfo.Type.Bold, TextRenderer.get()); + TextRenderer rowTextRenderer = Fonts.renderer("OnestMedium", FontInfo.Type.Regular, TextRenderer.get()); + if (headerIconRenderer == null) headerIconRenderer = textRenderer; + if (headerTextRenderer == null) headerTextRenderer = textRenderer; + if (rowTextRenderer == null) rowTextRenderer = textRenderer; + float baseScale = HudScale.scale(screenW, screenH) * 1.1f * HudPanelLayoutModes.effectiveScale(host.scaleValue); + float rowStep = Keybinds.ROW_STEP * baseScale; + float fontScale = 0.98f * (host.hud.getFontSize() / 18.0f); + headerIconRenderer.begin(fontScale * Keybinds.HEADER_ICON_SCALE, true, false); + float headerIconH = (float) headerIconRenderer.getHeight(false); + headerIconRenderer.end(); + headerTextRenderer.begin(fontScale, true, false); + float headerTextH = (float) headerTextRenderer.getHeight(false); + float titleWidth = (float) headerTextRenderer.getWidth("Keybinds", false); + headerTextRenderer.end(); + rowTextRenderer.begin(fontScale, true, false); + float rowTextH = (float) rowTextRenderer.getHeight(false); + float maxWidth = Keybinds.MIN_WIDTH * baseScale; + if (showExampleRow) { + float pw = (float) rowTextRenderer.getWidth("KillAuraG", false) + (34.0f * baseScale); + maxWidth = Math.max(maxWidth, pw); + } else { + for (Keybinds.AnimatedRow ar : animatedRows) { + Keybinds.Row r = ar.row(); + float w = (float) rowTextRenderer.getWidth(r.name() + r.bindText(), false) + (34.0f * baseScale); + maxWidth = Math.max(maxWidth, w); + } + } + rowTextRenderer.end(); + rowTextRenderer.begin(fontScale * 0.92f, false, false); + String activeCountText = Integer.toString(rows.size()); + float activeLabelWidth = (float) rowTextRenderer.getWidth("Active:", false); + float activeCountWidth = (float) rowTextRenderer.getWidth(activeCountText, false); + rowTextRenderer.end(); + float headerWidth = (Keybinds.TITLE_TEXT_X * baseScale) + titleWidth + activeLabelWidth + activeCountWidth + ((Keybinds.COUNT_LABEL_OFFSET + 12.0f) * baseScale); + maxWidth = Math.max(maxWidth, headerWidth); + float contentHeight = showExampleRow ? rowStep : KeybindsAnimator.totalAnimatedHeight(animatedRows, rowStep); + float targetHeight = (showExampleRow || !animatedRows.isEmpty()) ? (Keybinds.CONTENT_START_Y * baseScale + contentHeight) : (Keybinds.HEADER_HEIGHT * baseScale); + host.displayWidth = HudRenderUtil.animateDimension(host.displayWidth, maxWidth); + host.displayHeight = HudRenderUtil.animateDimension(host.displayHeight, targetHeight); + host.setSize(host.displayWidth, host.displayHeight); + if (!showWidget && widgetScale <= 0.001f && animatedRows.isEmpty()) { + host.setEmpty(); + return; + } + float drawScale = Math.max(0.0f, widgetScale); + float drawWidth = host.displayWidth * drawScale; + float drawHeight = host.displayHeight * drawScale; + if (drawWidth <= 0.0f || drawHeight <= 0.0f) { + host.setEmpty(); + return; + } + float drawX = host.getX() + ((host.displayWidth - drawWidth) * 0.5f); + float drawY = host.getY() + ((host.displayHeight - drawHeight) * 0.5f); + float drawBaseScale = baseScale * drawScale; + float drawFontScale = fontScale * drawScale; + int resolvedHeaderIconColor = host.uiCounter; + if (host.headerIconPulse.get()) { + int pulsed = HudTextEffects.animatedColor(host.uiCounter, HudTextEffects.Effect.PULSE, host.headerIconPulseSpeed.get(), (float) (Util.getMillis() / 1000.0), 0.0f); + resolvedHeaderIconColor = HudRenderUtil.mixColor(host.uiCounter, pulsed, host.headerIconPulseIntensity.get() / 100.0f); + } + List> panelRows = KeybindsData.buildPanelRows(host, animatedRows, showExampleRow); + host.scriptedPanel.render(renderer, textRenderer, ctx, tickDelta, new ScriptedListHudPanel.Panel( + ScriptedListHudPanel.KEYBINDS, + new ScriptedListHudPanel.Palette(host.uiHeaderLeft, host.uiHeaderRight, host.uiBodyLeft, host.uiBodyRight, host.uiOutline, host.uiText, host.uiMuted, host.uiCounter, host.uiTitleText, host.uiDivider, host.uiBlurTint), + drawX, drawY, drawWidth, drawHeight, drawScale, drawBaseScale, drawFontScale, + (float) headerIconH * drawScale, (float) headerTextH * drawScale, (float) rowTextH * drawScale, + activeLabelWidth * drawScale, activeCountWidth * drawScale, rows.size(), + KeybindsPalette.hasEffect(host), Math.min(1.0f, (host.blurAlpha.get() / 255.0f) * (KeybindsPalette.isThemeMode(host) ? 1.15f : 1.0f)), + resolvedHeaderIconColor, HudPanelLayoutModes.current(host.layoutMode), panelRows)); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/modulelist/ModuleListData.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/modulelist/ModuleListData.java new file mode 100644 index 0000000..32d2f2e --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/modulelist/ModuleListData.java @@ -0,0 +1,61 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.panels; + +import dev.loki.lovisual.features.module.core.Module; +import dev.loki.lovisual.features.module.core.ModuleManager; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; + +import java.util.Comparator; + +final class ModuleListData { + + private ModuleListData() {} + + static int collectEnabled(ModuleList host) { + host.enabledModules.clear(); + int hash = 1; + for (Module module : ModuleManager.getModules()) { + if (module != null && module.isEnabled() && module.isShownInModuleList()) { + host.enabledModules.add(module); + String label = module.getDisplayName(); + hash = 31 * hash + System.identityHashCode(module); + hash = 31 * hash + (label != null ? label.hashCode() : 0); + } + } + return hash; + } + + static boolean rebuildRowsIfNeeded(ModuleList host, TextRenderer listRenderer, float textScale, int modulesHash) { + boolean layoutChanged = modulesHash != host.cachedModulesHash + || host.enabledModules.size() != host.cachedModulesCount + || Math.abs(textScale - host.cachedTextScale) > 0.0001f; + if (!layoutChanged) return false; + host.rows.clear(); + listRenderer.begin(textScale, false, false); + for (Module module : host.enabledModules) { + ModuleList.Row row = new ModuleList.Row(module, module.getDisplayName()); + row.textWidth = (float) listRenderer.getWidth(row.label, false); + host.rows.add(row); + } + listRenderer.end(); + host.rows.sort(Comparator.comparingDouble((ModuleList.Row row) -> row.textWidth).reversed()); + host.cachedModulesHash = modulesHash; + host.cachedModulesCount = host.enabledModules.size(); + host.cachedTextScale = textScale; + return true; + } + + static float computeMaxWidth(ModuleList host, float stringPad, float rectExtra) { + float maxWidth = 0f; + for (ModuleList.Row row : host.rows) { + maxWidth = Math.max(maxWidth, row.textWidth + stringPad + rectExtra); + } + return maxWidth; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/modulelist/ModuleListPalette.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/modulelist/ModuleListPalette.java new file mode 100644 index 0000000..33ea1b4 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/modulelist/ModuleListPalette.java @@ -0,0 +1,120 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.panels; + +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; +import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; + +import static dev.loki.lovisual.features.theme.Theme.theme; + +final class ModuleListPalette { + + private ModuleListPalette() {} + + static boolean isThemeMode(ModuleList host) { + return host.colorMode.get() == ModuleList.ColorMode.THEME; + } + + static boolean isCustomMode(ModuleList host) { + return host.colorMode.get() == ModuleList.ColorMode.CUSTOM; + } + + static boolean isBlurEffect(ModuleList host) { + return host.bgEffect.get() == ModuleList.BgEffect.BLUR; + } + + static boolean isGlassEffect(ModuleList host) { + return host.bgEffect.get() == ModuleList.BgEffect.GLASS; + } + + static boolean hasEffect(ModuleList host) { + return host.bgEffect.get() != ModuleList.BgEffect.NONE; + } + + static boolean hasAnimatedTextGradient(ModuleList host) { + return host.textGradientMode.get() != AnimatedRenderColors.Mode.STATIC + || (isCustomMode(host) && (host.textTopColor.isRainbow() || host.textBottomColor.isRainbow())); + } + + static int resolveBgTop(ModuleList host) { + if (isThemeMode(host)) { + int base = HudRenderUtil.mixColor(theme().windowBg(), 0xFF000000, 0.38f); + return HudRenderUtil.setAlpha(base, host.bgAlpha.get()); + } + return host.bgTopColor.getArgb(); + } + + static int resolveBgBottom(ModuleList host) { + if (isThemeMode(host)) { + int base = HudRenderUtil.mixColor(theme().windowHeader(), 0xFF000000, 0.48f); + return HudRenderUtil.setAlpha(base, host.bgAlpha.get()); + } + return host.bgBottomColor.getArgb(); + } + + static int resolveTextTop(ModuleList host) { + if (isThemeMode(host)) { + return HudRenderUtil.mixColor(theme().accentSoft(), theme().accent(), 0.18f); + } + return host.textTopColor.getArgb(); + } + + static int resolveTextBottom(ModuleList host) { + if (isThemeMode(host)) { + return theme().accent(); + } + return host.textBottomColor.getArgb(); + } + + static int resolveTextGradientTop(ModuleList host) { + if (isThemeMode(host)) return resolveTextTop(host); + return host.textTopColor.isRainbow() + ? parseRgbFallback(host.textTopColor.rainbowFallbackHex()) : host.textTopColor.getArgb(); + } + + static int resolveTextGradientBottom(ModuleList host) { + if (isThemeMode(host)) return resolveTextBottom(host); + return host.textBottomColor.isRainbow() + ? parseRgbFallback(host.textBottomColor.rainbowFallbackHex()) : host.textBottomColor.getArgb(); + } + + static int parseRgbFallback(String hex) { + if (hex == null || hex.isBlank()) return 0xFFFFFFFF; + String s = hex.trim(); + if (s.startsWith("#")) s = s.substring(1); + if (s.startsWith("0x") || s.startsWith("0X")) s = s.substring(2); + try { + int value = (int) Long.parseUnsignedLong(s, 16); + if (s.length() <= 6) return 0xFF000000 | (value & 0x00FFFFFF); + return value; + } catch (NumberFormatException ignored) { + return 0xFFFFFFFF; + } + } + + static int rowFill(ModuleList host, int argb, int index, int total) { + if (!isThemeMode(host) || total <= 1) return argb; + float progress = (float) index / (float) (total - 1); + int alpha = (argb >>> 24) & 0xFF; + int rgb = HudRenderUtil.mixColor(argb, resolveTextBottom(host), 0.03f + 0.07f * progress); + return (rgb & 0x00FFFFFF) | (alpha << 24); + } + + static void drawGlass(ModuleList host, float x, float y, float w, float h, float radius) { + float blurStrength = host.blurAlpha.get() / 255f; + if (blurStrength <= 0.001f) return; + Renderer2D.COLOR.liquidGlassRect( + x, y, w, h, radius, + 0xFFFFFFFF, + 1.0f, + blurStrength, + Renderer2D.LiquidGlassPreset.BALANCED + ); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/modulelist/ModuleListRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/modulelist/ModuleListRenderer.java new file mode 100644 index 0000000..e1fec64 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/modulelist/ModuleListRenderer.java @@ -0,0 +1,150 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.panels; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.util.Util; +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; +import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors; +import dev.loki.lovisual.render.engine.math.HudScale; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.font.Fonts; +import dev.loki.lovisual.render.engine.text.font.info.FontInfo; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; + +final class ModuleListRenderer { + + private ModuleListRenderer() {} + + static void render(ModuleList host, Renderer2D renderer, TextRenderer textRenderer, + GuiGraphicsExtractor ctx, float tickDelta, int screenW, int screenH) { + Minecraft mc = Minecraft.getInstance(); + if (!host.isEnabled() || mc == null || mc.player == null) { + host.rows.clear(); + host.setSize(0f, 0f); + return; + } + + TextRenderer listRenderer = Fonts.renderer("Comfortaa", FontInfo.Type.Regular, TextRenderer.get()); + if (listRenderer == null) listRenderer = textRenderer; + + float baseScale = HudScale.scale(screenW, screenH) * host.scaleValue.get().floatValue(); + float lineH = ModuleList.BASE_LINE_H * baseScale; + float textOffsetY = ModuleList.BASE_TEXT_Y * baseScale; + float textOffsetX = ModuleList.BASE_TEXT_X * baseScale; + float textOffsetXRev = ModuleList.BASE_TEXT_X_REV * baseScale; + float stringPad = ModuleList.BASE_STRING_PAD * baseScale; + float rectExtra = ModuleList.BASE_RECT_EXTRA * baseScale; + float radius = ModuleList.BASE_RADIUS * baseScale; + float softness = ModuleList.BASE_SOFTNESS * baseScale; + + float fontHeight = Math.max(1.0f, (float) listRenderer.getHeight(false)); + float targetTextHeight = Math.max(4.4f * baseScale, lineH - 4.2f * baseScale); + float textScale = Math.max(0.2f, targetTextHeight / fontHeight); + + int modulesHash = ModuleListData.collectEnabled(host); + if (host.enabledModules.isEmpty()) { + host.rows.clear(); + host.setSize(0f, 0f); + return; + } + + ModuleListData.rebuildRowsIfNeeded(host, listRenderer, textScale, modulesHash); + + float maxWidth = ModuleListData.computeMaxWidth(host, stringPad, rectExtra); + float maxX = Math.max(0f, screenW - maxWidth); + host.clampX(maxX); + + boolean reverse = host.getX() > (screenW * 0.5f); + float drawBaseX = reverse ? (host.getX() + maxWidth) : host.getX(); + float listHeight = host.rows.size() * lineH; + + host.renderX = reverse ? (drawBaseX - maxWidth) : drawBaseX; + host.renderY = host.getY(); + host.setSize(maxWidth, listHeight); + + int bgTop = ModuleListPalette.resolveBgTop(host); + int bgBottom = ModuleListPalette.resolveBgBottom(host); + int textTop = ModuleListPalette.resolveTextTop(host); + int textBottom = ModuleListPalette.resolveTextBottom(host); + boolean blur = ModuleListPalette.isBlurEffect(host); + boolean glass = ModuleListPalette.isGlassEffect(host); + boolean shimmer = host.textEffect.get() == ModuleList.TextEffect.SHIMMER; + float time = (float) (Util.getMillis() / 1000.0); + float blurQuality = host.hud != null ? host.hud.getBlurRadius() : 12.0f; + boolean autoBatch = !Renderer2D.isBatching(); + if (autoBatch) renderer.begin(); + try { + for (int i = 0; i < host.rows.size(); i++) { + ModuleList.Row row = host.rows.get(i); + float offsetY = host.getY() + i * lineH; + float stringWidth = row.textWidth + stringPad; + float rowX = reverse ? (drawBaseX - stringWidth) : drawBaseX; + float rowW = stringWidth + rectExtra; + row.x = rowX; + row.y = offsetY; + row.w = rowW; + row.fillTop = ModuleListPalette.rowFill(host, bgTop, i, host.rows.size()); + row.fillBottom = ModuleListPalette.rowFill(host, bgBottom, i, host.rows.size()); + } + if (blur) { + renderer.blurComposite(composite -> { + for (int i = 0; i < host.rows.size(); i++) { + ModuleList.Row row = host.rows.get(i); + int tint = HudRenderUtil.mixColor(row.fillTop, row.fillBottom, 0.5f) & 0x00FFFFFF; + composite.roundedRect(row.x, row.y, row.w, lineH, radius, blurQuality, 1.0f, host.blurAlpha.get() / 255f, tint); + } + }); + } else if (glass) { + for (int i = 0; i < host.rows.size(); i++) { + ModuleList.Row row = host.rows.get(i); + ModuleListPalette.drawGlass(host, row.x, row.y, row.w, lineH, radius); + } + } + for (int i = 0; i < host.rows.size(); i++) { + ModuleList.Row row = host.rows.get(i); + if (glass) { + int glassTop = HudRenderUtil.scaleAlpha(row.fillTop, 0.22f); + int glassBottom = HudRenderUtil.scaleAlpha(row.fillBottom, 0.18f); + renderer.roundedRectGradientQuad(row.x, row.y, row.w, lineH, radius, softness, + glassTop, glassTop, glassBottom, glassBottom); + } else { + int drawTop = blur ? HudRenderUtil.scaleAlpha(row.fillTop, 0.42f) : row.fillTop; + int drawBottom = blur ? HudRenderUtil.scaleAlpha(row.fillBottom, 0.42f) : row.fillBottom; + renderer.roundedRectGradientQuad(row.x, row.y, row.w, lineH, radius, softness, + drawTop, drawTop, drawBottom, drawBottom); + } + } + } finally { + if (autoBatch) renderer.render(); + } + + int gradientTop = ModuleListPalette.resolveTextGradientTop(host); + int gradientBottom = ModuleListPalette.resolveTextGradientBottom(host); + boolean topRainbow = ModuleListPalette.isCustomMode(host) && host.textTopColor.isRainbow(); + boolean bottomRainbow = ModuleListPalette.isCustomMode(host) && host.textBottomColor.isRainbow(); + AnimatedRenderColors.Mode gradientMode = host.textGradientMode.get(); + int gradientSpeed = ModuleListPalette.hasAnimatedTextGradient(host) ? host.textGradientSpeed.get() : Math.max(1, host.textEffectSpeed.get()); + + listRenderer.begin(textScale, false, false); + float textHeight = (float) listRenderer.getHeight(false); + float blockTop = host.renderY; + float blockBottom = host.renderY + host.getHeight(); + for (ModuleList.Row row : host.rows) { + float textX = reverse ? (drawBaseX - (row.textWidth + stringPad) + textOffsetXRev) : (drawBaseX + textOffsetX); + float textY = row.y + textOffsetY + (lineH - textHeight - textOffsetY) * 0.08f; + listRenderer.renderQuadGradient(row.label, textX, textY, (idx, cp, x0, y0, x1, y1, out) -> + AnimatedRenderColors.moduleListBlockGlyphGradient( + gradientTop, gradientBottom, gradientMode, gradientSpeed, + y0, y1, blockTop, blockBottom, idx, time, topRainbow, bottomRainbow, shimmer, out + ), false); + } + listRenderer.end(); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/radar/RadarRectRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/radar/RadarRectRenderer.java new file mode 100644 index 0000000..c2fcfcc --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/radar/RadarRectRenderer.java @@ -0,0 +1,100 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.panels; + +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; +import dev.loki.lovisual.features.relations.category.CategoryService; +import dev.loki.lovisual.render.engine.math.HudScale; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import net.minecraft.client.Minecraft; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.player.Player; + +import static dev.loki.lovisual.features.theme.Theme.theme; + +final class RadarRectRenderer { + + private RadarRectRenderer() {} + + static void renderRect(Radar host, Renderer2D renderer, float tickDelta, int screenW, int screenH) { + float scale = HudScale.scale(screenW, screenH) + * (host.hud.getFontSize() / 18f); + float size = host.size.get().floatValue() * scale; + float baseX = host.getX(); + float baseY = host.getY(); + float radius = Math.min(Radar.BASE_RADIUS * scale, size * 0.25f); + float lineW = Math.max(0.5f, Radar.BASE_LINE_W * scale); + float pad = Radar.BASE_PAD * scale; + + int bgAlpha = host.bgAlpha.get(); + boolean syncTheme = host.colorMode.get() == Radar.ColorMode.SYNC; + int bgRgb = syncTheme + ? (theme().windowBg() & 0x00FFFFFF) + : (host.bgColor.getArgb() & 0x00FFFFFF); + int bg = (bgAlpha << 24) | bgRgb; + + boolean blurEnabled = host.blur.get(); + if (blurEnabled) { + drawBlur(host, baseX, baseY, size, size, radius, bg); + } + boolean useGradient = syncTheme && host.gradient.get(); + HudRenderUtil.drawHudBackground(renderer, baseX, baseY, size, size, radius, 1.0f, bg, useGradient); + + int stroke = HudRenderUtil.setAlpha(theme().accent(), 0x55); + renderer.roundedRectStroke(baseX, baseY, size, size, radius, 1.0f, + Math.max(1.0f, 1.25f * scale), stroke); + + int cross = HudRenderUtil.setAlpha(theme().textMuted(), 0x55); + float cx = baseX + size * 0.5f; + float cy = baseY + size * 0.5f; + renderer.quad(cx - lineW * 0.5f, baseY + pad, lineW, size - pad * 2f, cross); + renderer.quad(baseX + pad, cy - lineW * 0.5f, size - pad * 2f, lineW, cross); + + renderBlips(renderer, tickDelta, cx, cy, size * 0.5f - Radar.BLIP_MARGIN * scale, scale); + + host.setSize(size, size); + } + + static void renderBlips(Renderer2D renderer, float tickDelta, float cx, float cy, float half, float scale) { + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.player == null || mc.level == null) return; + + float blip = Math.max(2.5f, Radar.BLIP_SIZE * scale); + float blipRadius = Math.max(1.0f, blip * 0.5f); + float yaw = mc.player.getViewYRot(tickDelta); + float yawRad = (float) Math.toRadians(yaw); + float cos = Mth.cos(yawRad); + float sin = Mth.sin(yawRad); + + for (Player player : mc.level.players()) { + if (player == mc.player) continue; + + double px = player.getX(); + double pz = player.getZ(); + double dx = px - mc.player.getX(); + double dz = pz - mc.player.getZ(); + + float rotY = (float) (-(dz * cos - dx * sin)) * 2.0f; + float rotX = (float) (-(dx * cos + dz * sin)) * 2.0f; + + rotX = Mth.clamp(rotX, -half, half); + rotY = Mth.clamp(rotY, -half, half); + + int color = CategoryService.getColor(player); + renderer.roundedRect(cx + rotX - blip * 0.5f, cy + rotY - blip * 0.5f, + blip, blip, blipRadius, 1.0f, color); + } + } + + static void drawBlur(Radar host, float x, float y, float w, float h, float radius, int tintRgb) { + float quality = host.hud.getBlurRadius(); + float brightness = 1.0f; + float alpha = host.blurAlpha.get() / 255f; + Renderer2D.COLOR.blurRect(x, y, w, h, radius, quality, brightness, alpha, 0xFFFFFF); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/radar/RadarTextRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/radar/RadarTextRenderer.java new file mode 100644 index 0000000..1948ba8 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/radar/RadarTextRenderer.java @@ -0,0 +1,124 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.panels; + +import dev.loki.lovisual.render.engine.color.RenderColor; +import dev.loki.lovisual.render.engine.math.HudScale; +import dev.loki.lovisual.render.engine.text.font.Fonts; +import dev.loki.lovisual.render.engine.text.font.info.FontInfo; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.util.player.health.PlayerHealthResolver; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.PlayerInfo; +import net.minecraft.world.entity.player.Player; + +import static dev.loki.lovisual.features.theme.Theme.theme; + +final class RadarTextRenderer { + + private RadarTextRenderer() {} + + static void renderText(Radar host, TextRenderer textRenderer, int screenW, int screenH, boolean preview) { + Minecraft mc = Minecraft.getInstance(); + boolean ready = mc != null && mc.player != null && mc.level != null; + if (!ready && !preview) { + host.setEmpty(); + return; + } + + TextRenderer fallback = textRenderer != null ? textRenderer : TextRenderer.get(); + TextRenderer renderer = Fonts.renderer("Iosevka", FontInfo.Type.Regular, fallback); + + float scale = HudScale.scale(screenW, screenH) + * (host.hud.getFontSize() / 18f) + * Radar.TEXT_SCALE_MULT; + + renderer.begin(scale, false, false); + float lineH = (float) renderer.getHeight(false); + renderer.end(); + + int textColor = host.colorMode.get() == Radar.ColorMode.CUSTOM + ? 0xFF000000 | (host.textColor.getArgb() & 0x00FFFFFF) + : theme().accent(); + + float maxW = 0f; + float yOff = 0f; + if (ready) { + for (Player player : mc.level.players()) { + if (player == mc.player) continue; + String line = buildLine(host, player); + if (line.isEmpty()) continue; + + renderer.begin(scale, false, false); + float w = (float) renderer.getWidth(line, false); + renderer.render(line, host.getX(), host.getY() + yOff, new RenderColor(textColor), false); + renderer.end(); + + maxW = Math.max(maxW, w); + yOff += lineH; + } + } + + if (preview && yOff <= 0f) { + String line = "Player 20 40ms"; + renderer.begin(scale, false, false); + float w = (float) renderer.getWidth(line, false); + renderer.render(line, host.getX(), host.getY(), new RenderColor(textColor), false); + renderer.end(); + maxW = w; + yOff = lineH; + } + + host.setSize(maxW, Math.max(lineH, yOff)); + } + + static String buildLine(Radar host, Player player) { + StringBuilder out = new StringBuilder(64); + appendField(host, out, host.field1.get(), player); + appendField(host, out, host.field2.get(), player); + appendField(host, out, host.field3.get(), player); + appendField(host, out, host.field4.get(), player); + appendField(host, out, host.field5.get(), player); + return out.toString().trim(); + } + + static void appendField(Radar host, StringBuilder out, Radar.Field field, Player player) { + Minecraft mc = Minecraft.getInstance(); + if (field == null || field == Radar.Field.NONE) return; + if (out.length() > 0) out.append(' '); + switch (field) { + case HP -> { + int hp = (int) Math.ceil(PlayerHealthResolver.totalHealth(player)); + out.append(hp); + } + case DISTANCE -> { + int dist = (int) Math.ceil(mc.player.distanceTo(player)); + out.append(dist).append('m'); + } + case PING -> { + int ping = getPlayerPing(player); + if (ping >= 0) { + out.append(ping).append("ms"); + } else { + out.append("--"); + } + } + case NAME -> out.append(player.getName().getString()); + default -> { + // ignore + } + } + } + + static int getPlayerPing(Player player) { + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.getConnection() == null) return -1; + PlayerInfo entry = mc.getConnection().getPlayerInfo(player.getUUID()); + return entry != null ? entry.getLatency() : -1; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/scoreboard/ScoreboardDataHelper.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/scoreboard/ScoreboardDataHelper.java new file mode 100644 index 0000000..7f055bc --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/scoreboard/ScoreboardDataHelper.java @@ -0,0 +1,154 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.panels; + +import dev.loki.lovisual.features.module.lifecycle.Modules; +import dev.loki.lovisual.features.module.modules.visuals.visibility.NoRender; +import dev.loki.lovisual.util.logging.ServerDumpUtil; +import dev.loki.lovisual.util.text.format.LegacyTextUtil; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.chat.numbers.NumberFormat; +import net.minecraft.network.chat.numbers.StyledFormat; +import net.minecraft.world.scores.DisplaySlot; +import net.minecraft.world.scores.Objective; +import net.minecraft.world.scores.PlayerScoreEntry; +import net.minecraft.world.scores.PlayerTeam; + +import dev.loki.lovisual.util.input.KeyManager; +import dev.loki.lovisual.util.screen.ClientScreen; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +final class ScoreboardDataHelper { + + private static final Comparator ENTRY_COMPARATOR = + Comparator.comparingInt(PlayerScoreEntry::value).reversed() + .thenComparing(PlayerScoreEntry::owner, String.CASE_INSENSITIVE_ORDER); + + static final List PREVIEW_SAMPLE = List.of( + new Scoreboard.SidebarLine(Component.literal("Entry One"), Component.literal("12"), false), + new Scoreboard.SidebarLine(Component.literal("Entry Two"), Component.literal("34"), false), + new Scoreboard.SidebarLine(Component.literal("Entry Three"), Component.literal("56"), false), + new Scoreboard.SidebarLine(Component.literal("Entry Four"), Component.literal("78"), false), + new Scoreboard.SidebarLine(Component.literal("Entry Five"), Component.literal("90"), false), + new Scoreboard.SidebarLine(Component.literal("Entry Six"), Component.literal("12%"), false), + new Scoreboard.SidebarLine(Component.literal("Entry Seven"), Component.literal("3"), false), + new Scoreboard.SidebarLine(Component.literal("Footer Text"), Component.empty(), true) + ); + + private ScoreboardDataHelper() {} + + static Objective resolveObjective() { + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.level == null || mc.player == null) return null; + net.minecraft.world.scores.Scoreboard scoreboard = mc.level.getScoreboard(); + Objective objective = null; + PlayerTeam team = scoreboard.getPlayersTeam(mc.player.getScoreboardName()); + if (team != null) { + DisplaySlot slot = team.getColor().map(net.minecraft.world.scores.TeamColor::displaySlot).orElse(null); + if (slot != null) objective = scoreboard.getDisplayObjective(slot); + } + return objective != null ? objective : scoreboard.getDisplayObjective(DisplaySlot.SIDEBAR); + } + + static Component resolveTitle(Objective objective, boolean preview) { + if (objective == null) return preview ? Component.literal("Scoreboard") : Component.empty(); + Component title = LegacyTextUtil.convertLegacyCodes(objective.getDisplayName()); + return isBlankText(title) ? Component.literal("Scoreboard") : title; + } + + static List resolveLines(Objective objective, boolean preview) { + if (objective == null) return preview ? PREVIEW_SAMPLE : List.of(); + net.minecraft.world.scores.Scoreboard scoreboard = objective.getScoreboard(); + NumberFormat numberFormat = objective.numberFormatOrDefault(StyledFormat.SIDEBAR_DEFAULT); + List resolved = new ArrayList<>(); + List visible = scoreboard.listPlayerScores(objective).stream() + .filter(e -> !e.isHidden()).sorted(ENTRY_COMPARATOR).limit(15).toList(); + for (PlayerScoreEntry entry : visible) { + PlayerTeam team = scoreboard.getPlayersTeam(entry.owner()); + Component label = LegacyTextUtil.convertLegacyCodes(PlayerTeam.formatNameForTeam(team, entry.ownerName())); + Component value = LegacyTextUtil.convertLegacyCodes(entry.formatValue(numberFormat)); + if (isBlankText(label) && isBlankText(value)) continue; + resolved.add(new Scoreboard.SidebarLine(label, value, isBlankText(value))); + } + ServerDumpUtil.dumpScoreboardSidebar(objective, visible, + resolved.stream().map(l -> l.label().getString()).toList(), + resolved.stream().map(l -> l.value().getString()).toList()); + return resolved.isEmpty() && preview ? PREVIEW_SAMPLE : resolved; + } + + static void toggleNoRenderHidden() { + NoRender noRender = Modules.get(NoRender.class); + if (noRender == null) return; + noRender.setHideScoreboardRaw(!noRender.hideScoreboardRaw()); + } + + static void handleTick(Scoreboard board) { + Minecraft mc = Minecraft.getInstance(); + if (!board.toggleInit) { + if (mc == null || mc.getWindow() == null) return; + board.toggleArmed = true; + String combo = board.toggleBind.get(); + if (combo != null && !combo.isBlank() && !"NONE".equalsIgnoreCase(combo) + && ClientScreen.current() == null && KeyManager.isComboHeldAllowScreen(combo)) { + board.toggleArmed = false; + } + board.toggleInit = true; + } + String combo = board.toggleBind.get(); + if (combo == null || combo.isBlank() || "NONE".equalsIgnoreCase(combo)) return; + if (mc == null || mc.getWindow() == null) return; + boolean pressed = ClientScreen.current() == null && KeyManager.isComboHeldAllowScreen(combo); + if (!pressed) { + board.toggleArmed = true; + return; + } + if (board.toggleArmed) { + toggleNoRenderHidden(); + board.toggleArmed = false; + } + } + + static boolean isBlankText(Component text) { + return text == null || text.getString().isBlank(); + } + + static Component normalizeHeaderTitle(Component text) { + if (text == null) return Component.empty(); + MutableComponent out = Component.empty(); + text.visit((style, value) -> { + if (value != null && !value.isEmpty()) { + out.append(Component.literal(normalizeHeaderGlyphs(value)).setStyle(style)); + } + return java.util.Optional.empty(); + }, net.minecraft.network.chat.Style.EMPTY); + return out; + } + + static String normalizeHeaderGlyphs(String input) { + if (input == null || input.isEmpty()) return ""; + StringBuilder out = new StringBuilder(input.length()); + for (int i = 0; i < input.length(); i++) { + char ch = input.charAt(i); + out.append(switch (ch) { + case 'ᴀ' -> 'a'; case 'ʙ' -> 'b'; case 'ᴄ' -> 'c'; case 'ᴅ' -> 'd'; + case 'ᴇ' -> 'e'; case 'ꜰ' -> 'f'; case 'ɢ' -> 'g'; case 'ʜ' -> 'h'; + case 'ɪ' -> 'i'; case 'ᴊ' -> 'j'; case 'ᴋ' -> 'k'; case 'ʟ' -> 'l'; + case 'ᴍ' -> 'm'; case 'ɴ' -> 'n'; case 'ᴏ' -> 'o'; case 'ᴘ' -> 'p'; + case 'ǫ' -> 'q'; case 'ʀ' -> 'r'; case 'ꜱ' -> 's'; case 'ᴛ' -> 't'; + case 'ᴜ' -> 'u'; case 'ᴠ' -> 'v'; case 'ᴡ' -> 'w'; case 'x' -> 'x'; + case 'ʏ' -> 'y'; case 'ᴢ' -> 'z'; default -> ch; + }); + } + return out.toString(); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/scoreboard/ScoreboardRenderHelper.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/scoreboard/ScoreboardRenderHelper.java new file mode 100644 index 0000000..2435548 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/scoreboard/ScoreboardRenderHelper.java @@ -0,0 +1,152 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.panels; + +import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; +import dev.loki.lovisual.render.engine.math.HudScale; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.font.Fonts; +import dev.loki.lovisual.render.engine.text.font.info.FontInfo; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.network.chat.Component; +import net.minecraft.world.scores.Objective; + +import static dev.loki.lovisual.features.theme.Theme.theme; + +final class ScoreboardRenderHelper { + + private ScoreboardRenderHelper() {} + + static void updatePalette(Scoreboard board) { + if (board.isThemeMode()) { + int panelAlpha = board.bgAlpha.get(); + board.uiBgPrimary = HudRenderUtil.setAlpha(theme().windowBg(), panelAlpha); + board.uiBgSecondary = HudRenderUtil.setAlpha( + HudRenderUtil.mixColor(theme().surface(), theme().windowHeader(), 0.35f), panelAlpha); + board.uiHeaderBg = HudRenderUtil.setAlpha(theme().windowHeader(), panelAlpha); + board.uiStroke = HudRenderUtil.setAlpha( + HudRenderUtil.mixColor(theme().windowStroke(), theme().strokeSoft(), 0.4f), + Math.min(panelAlpha, 190)); + board.uiTitleColor = theme().textPrimary(); + board.uiLabelColor = theme().textMuted(); + board.uiValueColor = theme().textPrimary(); + board.uiFooterColor = HudRenderUtil.mixColor(theme().accent(), theme().textPrimary(), 0.35f); + return; + } + board.uiBgPrimary = board.bg.getArgb(); + board.uiBgSecondary = board.bg2.getArgb(); + board.uiHeaderBg = HudRenderUtil.mixColor(board.bg2.getArgb(), board.bg.getArgb(), 0.2f); + board.uiStroke = board.stroke.getArgb(); + board.uiTitleColor = board.titleColor.getArgb(); + board.uiLabelColor = board.labelColor.getArgb(); + board.uiValueColor = board.valueColor.getArgb(); + board.uiFooterColor = board.footerColor.getArgb(); + } + + static void drawBlur(Scoreboard board, float x, float y, float w, float h, float radius) { + if (!board.hasEffect()) return; + float quality = board.hud.getBlurRadius(); + float alpha = board.blurAlpha.get() / 255f; + Renderer2D.COLOR.blurRect(x, y, w, h, radius, quality, 1.0f, alpha, 0xFFFFFF); + } + + static void drawBackground(Renderer2D renderer, Scoreboard board, float x, float y, float w, float h, float radius, float s) { + if (board.isThemeMode()) { + HudRenderUtil.drawHudBackground(renderer, x, y, w, h, radius, Scoreboard.BASE_SOFTNESS, board.uiBgPrimary, true); + } else { + renderer.roundedRectGradientQuad(x, y, w, h, radius, Scoreboard.BASE_SOFTNESS, board.uiBgPrimary, board.uiBgSecondary, board.uiBgPrimary, board.uiBgSecondary); + } + renderer.roundedRectStroke(x, y, w, h, radius, Scoreboard.BASE_SOFTNESS, Math.max(0.5f, Scoreboard.BASE_STROKE * s), board.uiStroke); + } + + static void drawHeader(Renderer2D renderer, Scoreboard board, float x, float y, float w, float hh, float radius) { + renderer.roundedRectCorners(x, y, w, hh, radius, radius, 0.0f, 0.0f, Scoreboard.BASE_SOFTNESS, board.uiHeaderBg); + } + + static void render(Scoreboard board, Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta, int screenW, int screenH) { + Minecraft mc = Minecraft.getInstance(); + boolean preview = DraggableHudElementRegistry.isForceVisible(); + if (mc == null || (mc.level == null && !preview)) { board.setSize(0.0f, 0.0f); return; } + if (!preview && Scoreboard.isHiddenByNoRender()) { board.setSize(0.0f, 0.0f); return; } + if (!preview && !board.isEnabled()) { board.setSize(0.0f, 0.0f); return; } + Objective objective = ScoreboardDataHelper.resolveObjective(); + if (objective == null && !preview) { board.setSize(0.0f, 0.0f); return; } + updatePalette(board); + Component title = ScoreboardDataHelper.resolveTitle(objective, preview); + board.lines.clear(); + board.lines.addAll(ScoreboardDataHelper.resolveLines(objective, preview)); + if (board.lines.isEmpty()) { board.setSize(0.0f, 0.0f); return; } + float baseScale = HudScale.scale(screenW, screenH) * board.scale.get().floatValue(); + float padX = Scoreboard.BASE_PAD_X * baseScale, padY = Scoreboard.BASE_PAD_Y * baseScale; + float rowGap = Scoreboard.BASE_ROW_GAP * baseScale, scoreGap = Scoreboard.BASE_SCORE_GAP * baseScale; + float footerGap = Scoreboard.BASE_FOOTER_GAP * baseScale, radius = Scoreboard.BASE_RADIUS * baseScale; + float titleScale = Scoreboard.BASE_TITLE_SCALE * baseScale, textScale = Scoreboard.BASE_TEXT_SCALE * baseScale; + float time = (float) (net.minecraft.util.Util.getMillis() / 1000.0); + HudTextEffects.Effect effect = board.textEffect.get(); + int effectSpeed = board.textEffectSpeed.get(); + TextRenderer fallback = textRenderer != null ? textRenderer : TextRenderer.get(); + TextRenderer titleRenderer = Fonts.renderer("Iosevka", FontInfo.Type.BoldItalic, fallback); + TextRenderer bodyRenderer = Fonts.renderer("Iosevka", FontInfo.Type.Regular, titleRenderer); + Component normalizedTitle = ScoreboardDataHelper.normalizeHeaderTitle(title); + var titleLines = ScoreboardTextHelper.splitStyledLines(normalizedTitle, board.uiTitleColor); + float titleW = ScoreboardTextHelper.measureStyledLineWidth(titleRenderer, titleLines, titleScale); + float titleH = ScoreboardTextHelper.rendererHeight(titleRenderer, titleScale); + float textH = ScoreboardTextHelper.rendererHeight(bodyRenderer, textScale); + float maxLabelW = 0.0f, maxValueW = 0.0f, maxFooterW = 0.0f; + for (Scoreboard.SidebarLine line : board.lines) { + if (line.footer()) maxFooterW = Math.max(maxFooterW, ScoreboardTextHelper.measureStyledText(bodyRenderer, line.label(), board.uiFooterColor, textScale)); + else { + maxLabelW = Math.max(maxLabelW, ScoreboardTextHelper.measureStyledText(bodyRenderer, line.label(), board.uiLabelColor, textScale)); + if (!ScoreboardDataHelper.isBlankText(line.value())) maxValueW = Math.max(maxValueW, ScoreboardTextHelper.measureStyledText(bodyRenderer, line.value(), board.uiValueColor, textScale)); + } + } + board.displayLabelWidth = Scoreboard.smoothWidth(board.displayLabelWidth, maxLabelW); + board.displayValueWidth = Scoreboard.smoothWidth(board.displayValueWidth, maxValueW); + board.displayFooterWidth = Scoreboard.smoothWidth(board.displayFooterWidth, maxFooterW); + float widestRow = board.displayLabelWidth + (board.displayValueWidth > 0.0f ? scoreGap + board.displayValueWidth : 0.0f); + float widest = Math.max(titleW, Math.max(widestRow, board.displayFooterWidth)); + float width = padX * 2.0f + widest; + float titleLineGap = Scoreboard.BASE_TITLE_LINE_GAP * baseScale; + float titleBlockHeight = titleLines.size() * titleH + Math.max(0, titleLines.size() - 1) * titleLineGap; + float headerHeight = titleBlockHeight + padY * 2.0f; + int footerCount = 0, bodyCount = 0; + for (Scoreboard.SidebarLine line : board.lines) if (line.footer()) footerCount++; else bodyCount++; + float rowsH = bodyCount > 0 ? bodyCount * textH + Math.max(0, bodyCount - 1) * rowGap : 0.0f; + float footersH = footerCount > 0 ? footerCount * textH + Math.max(0, footerCount - 1) * rowGap : 0.0f; + float footerSectionGap = footerCount > 0 && bodyCount > 0 ? footerGap : 0.0f; + float height = headerHeight + padY + rowsH + footerSectionGap + footersH + padY; + board.setSize(width, height); + float x = board.getX(); + float y = board.getY(); + if (board.isBlurEffect()) drawBlur(board, x, y, width, height, radius); + drawBackground(renderer, board, x, y, width, height, radius, baseScale); + drawHeader(renderer, board, x, y, width, headerHeight, radius); + float cursorY = y + headerHeight + padY; + float titleY = y + (headerHeight - titleBlockHeight) * 0.5f; + ScoreboardTextRenderer.renderStyledLinesCentered(renderer, titleRenderer, titleScale, x, width, titleY, titleLines, titleLineGap, board.textEffectSections.get("header"), effect, effectSpeed, time); + boolean footerStarted = false; + for (Scoreboard.SidebarLine line : board.lines) { + if (line.footer()) { + if (!footerStarted && bodyCount > 0) { cursorY += footerGap; footerStarted = true; } + ScoreboardTextRenderer.renderStyledText(renderer, bodyRenderer, textScale, x + padX, cursorY, line.label(), board.uiFooterColor, board.textEffectSections.get("footer"), effect, effectSpeed, time, 0.0f); + cursorY += textH + rowGap; continue; + } + ScoreboardTextRenderer.renderStyledText(renderer, bodyRenderer, textScale, x + padX, cursorY, line.label(), board.uiLabelColor, board.textEffectSections.get("labels"), effect, effectSpeed, time, 0.0f); + if (!ScoreboardDataHelper.isBlankText(line.value())) { + float valueW = ScoreboardTextHelper.measureStyledText(bodyRenderer, line.value(), board.uiValueColor, textScale); + float valueX = x + width - padX - valueW; + ScoreboardTextRenderer.renderStyledText(renderer, bodyRenderer, textScale, valueX, cursorY, line.value(), board.uiValueColor, board.textEffectSections.get("values"), effect, effectSpeed, time, 0.2f); + } + cursorY += textH + rowGap; + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/scoreboard/ScoreboardTextHelper.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/scoreboard/ScoreboardTextHelper.java new file mode 100644 index 0000000..b822778 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/scoreboard/ScoreboardTextHelper.java @@ -0,0 +1,143 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.panels; + +import dev.loki.lovisual.render.engine.text.font.Fonts; +import dev.loki.lovisual.render.engine.text.font.info.FontInfo; +import dev.loki.lovisual.render.engine.text.glyph.custom.TextGlyphFallback; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.util.text.format.TextRenderUtil; +import dev.loki.lovisual.util.text.format.TextRenderUtil.Part; +import net.minecraft.network.chat.Component; + +import java.util.ArrayList; +import java.util.List; + +final class ScoreboardTextHelper { + + private ScoreboardTextHelper() {} + + static float measureStyledText(TextRenderer fallback, Component text, int defaultColor, float scale) { + if (fallback == null || text == null) return 0.0f; + float width = 0.0f; + for (Part part : TextRenderUtil.flattenStyled(text, defaultColor)) { + width += partWidth(fallback, part, scale, 0.0f, 0); + } + return width; + } + + static List> splitStyledLines(Component text, int defaultColor) { + List> lines = new ArrayList<>(); + List current = new ArrayList<>(); + lines.add(current); + for (Part part : TextRenderUtil.flattenStyled(text, defaultColor)) { + String value = part.text(); + int start = 0; + while (true) { + int nl = value.indexOf('\n', start); + String segment = nl >= 0 ? value.substring(start, nl) : value.substring(start); + if (!segment.isEmpty()) current.add(new Part(segment, part.color(), part.bold(), + part.italic(), part.underline(), part.strikethrough(), part.obfuscated())); + if (nl < 0) break; + current = new ArrayList<>(); + lines.add(current); + start = nl + 1; + } + } + return lines; + } + + static float measureStyledLineWidth(TextRenderer fallback, List> lines, float scale) { + if (fallback == null || lines == null || lines.isEmpty()) return 0.0f; + float widest = 0.0f; + for (List line : lines) { + float width = 0.0f; + int idx = 0; + for (Part part : line) width += partWidth(fallback, part, scale, 0.0f, idx++); + widest = Math.max(widest, width); + } + return widest; + } + + static float rendererHeight(TextRenderer renderer, float scale) { + if (renderer == null) return 0.0f; + renderer.begin(scale, false, false); + float h = (float) renderer.getHeight(false); + renderer.end(); + return h; + } + + static TextRenderer styledRenderer(TextRenderer fallback, Part part) { + if (part == null) return fallback; + FontInfo.Type type; + if (part.bold() && part.italic()) type = FontInfo.Type.BoldItalic; + else if (part.bold()) type = FontInfo.Type.Bold; + else if (part.italic()) type = FontInfo.Type.Italic; + else type = FontInfo.Type.Regular; + TextRenderer r = Fonts.renderer("Iosevka", type, fallback); + return r != null ? r : fallback; + } + + static TextRenderer rendererForGlyph(TextRenderer preferred, int codePoint) { + return TextGlyphFallback.rendererForGlyph(preferred, codePoint); + } + + static float svgGlyphSize(TextRenderer renderer, float scale) { + return Math.max(1.0f, rendererHeight(renderer, scale)) * 0.92f; + } + + static float partWidth(TextRenderer fallback, Part part, float scale, float timeSec, int salt) { + if (fallback == null || part == null || part.text().isEmpty()) return 0.0f; + TextRenderer renderer = styledRenderer(fallback, part); + String text = part.obfuscated() ? obfuscate(part.text(), timeSec, salt) : part.text(); + return measureTextRuns(renderer, scale, text); + } + + static float measureTextRuns(TextRenderer preferred, float scale, String text) { + if (preferred == null || text == null || text.isEmpty()) return 0.0f; + TextRenderer current = null; + float width = 0.0f; + float svgAdvance = svgGlyphSize(preferred, scale); + try { + for (int i = 0; i < text.length(); ) { + int cp = text.codePointAt(i); + if (TextGlyphFallback.shouldUseVanillaSvg(preferred, cp)) { + if (current != null) { current.end(); current = null; } + width += svgAdvance; + i += Character.charCount(cp); + continue; + } + String glyph = new String(Character.toChars(cp)); + TextRenderer next = rendererForGlyph(preferred, cp); + if (next != current) { + if (current != null) current.end(); + current = next; + current.begin(scale, false, false); + } + width += (float) current.getWidth(glyph, false); + i += Character.charCount(cp); + } + return width; + } finally { + if (current != null) current.end(); + } + } + + static String obfuscate(String text, float timeSec, int salt) { + if (text == null || text.isEmpty()) return ""; + final String alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + int seed = Math.abs((int) (timeSec * 18.0f) * 31 + salt * 131); + StringBuilder out = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char ch = text.charAt(i); + if (Character.isWhitespace(ch)) out.append(ch); + else out.append(alphabet.charAt(Math.floorMod(seed + i * 17 + ch, alphabet.length()))); + } + return out.toString(); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/scoreboard/ScoreboardTextRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/scoreboard/ScoreboardTextRenderer.java new file mode 100644 index 0000000..12e8894 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/scoreboard/ScoreboardTextRenderer.java @@ -0,0 +1,138 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.panels; + +import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; +import dev.loki.lovisual.render.engine.color.RenderColor; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.svg.model.SvgRenderOptions; +import dev.loki.lovisual.render.engine.text.glyph.custom.TextGlyphFallback; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.util.text.format.TextRenderUtil.Part; +import net.minecraft.network.chat.Component; + +import java.util.List; + +import static dev.loki.lovisual.features.gui.hud.draggable.impl.panels.ScoreboardTextHelper.*; + +final class ScoreboardTextRenderer { + + private ScoreboardTextRenderer() {} + + static void renderStyledText(Renderer2D shapeRenderer, TextRenderer fallback, float scale, + float x, float y, Component text, int defaultColor, boolean animated, + HudTextEffects.Effect effect, int effectSpeed, float timeSec, float phase) { + if (fallback == null || text == null) return; + float cursorX = x; + int partIndex = 0; + float lineHeight = rendererHeight(fallback, scale); + for (Part part : dev.loki.lovisual.util.text.format.TextRenderUtil.flattenStyled(text, defaultColor)) { + TextRenderer renderer = styledRenderer(fallback, part); + String renderText = part.obfuscated() ? obfuscate(part.text(), timeSec + phase, partIndex) : part.text(); + float partW = renderTextRuns(shapeRenderer, renderer, scale, cursorX, y, renderText, part.color(), + animated, effect, effectSpeed, timeSec + phase + partIndex * 0.12f); + renderDecorations(shapeRenderer, cursorX, y, partW, lineHeight, part); + cursorX += partW; + partIndex++; + } + } + + static void renderStyledLinesCentered(Renderer2D shapeRenderer, TextRenderer fallback, float scale, + float boxX, float boxWidth, float y, List> lines, + float lineGap, boolean animated, HudTextEffects.Effect effect, + int effectSpeed, float timeSec) { + if (fallback == null || lines == null || lines.isEmpty()) return; + float lineHeight = rendererHeight(fallback, scale); + float cursorY = y; + for (int lineIndex = 0; lineIndex < lines.size(); lineIndex++) { + List line = lines.get(lineIndex); + float lineWidth = 0.0f; + int measurePart = 0; + for (Part part : line) lineWidth += partWidth(fallback, part, scale, timeSec + lineIndex * 0.18f, measurePart++); + float cursorX = boxX + Math.max(0.0f, (boxWidth - lineWidth) * 0.5f); + int partIndex = 0; + for (Part part : line) { + TextRenderer renderer = styledRenderer(fallback, part); + String renderText = part.obfuscated() ? obfuscate(part.text(), timeSec + lineIndex * 0.18f, partIndex) : part.text(); + float partW = renderTextRuns(shapeRenderer, renderer, scale, cursorX, cursorY, renderText, part.color(), + animated, effect, effectSpeed, timeSec + lineIndex * 0.18f + partIndex * 0.12f); + renderDecorations(shapeRenderer, cursorX, cursorY, partW, lineHeight, part); + cursorX += partW; + partIndex++; + } + cursorY += lineHeight + lineGap; + } + } + + static float renderTextRuns(Renderer2D shapeRenderer, TextRenderer preferred, float scale, float x, float y, + String text, int color, boolean animated, HudTextEffects.Effect effect, + int effectSpeed, float timeSec) { + if (preferred == null || text == null || text.isEmpty()) return 0.0f; + TextRenderer current = null; + StringBuilder run = new StringBuilder(); + float cursorX = x; + float runX = x; + int glyphIndex = 0; + float svgSize = svgGlyphSize(preferred, scale); + float svgY = y + (rendererHeight(preferred, scale) - svgSize) * 0.5f; + for (int i = 0; i < text.length(); ) { + int cp = text.codePointAt(i); + if (TextGlyphFallback.shouldUseVanillaSvg(preferred, cp)) { + if (current != null && !run.isEmpty()) { + cursorX = flushTextRun(current, run.toString(), scale, runX, y, color, animated, effect, effectSpeed, timeSec + glyphIndex * 0.12f); + run.setLength(0); + } + current = null; + String svgName = TextGlyphFallback.vanillaSvgName(cp); + if (shapeRenderer != null && svgName != null) { + shapeRenderer.svg(svgName, cursorX, svgY, svgSize, svgSize, + SvgRenderOptions.fromFile().withAlpha(((color >>> 24) & 0xFF) / 255.0f)); + } + cursorX += svgSize; + runX = cursorX; + i += Character.charCount(cp); + glyphIndex++; + continue; + } + TextRenderer next = rendererForGlyph(preferred, cp); + if (current != null && next != current) { + cursorX = flushTextRun(current, run.toString(), scale, runX, y, color, animated, effect, effectSpeed, timeSec + glyphIndex * 0.12f); + run.setLength(0); + runX = cursorX; + } + if (next != current) current = next; + run.appendCodePoint(cp); + i += Character.charCount(cp); + glyphIndex++; + } + if (current != null && !run.isEmpty()) { + cursorX = flushTextRun(current, run.toString(), scale, runX, y, color, animated, effect, effectSpeed, timeSec + glyphIndex * 0.12f); + } + return cursorX - x; + } + + static float flushTextRun(TextRenderer renderer, String text, float scale, float x, float y, int color, + boolean animated, HudTextEffects.Effect effect, int effectSpeed, float timeSec) { + if (renderer == null || text == null || text.isEmpty()) return x; + renderer.begin(scale, false, false); + boolean rendered = animated && HudTextEffects.render(renderer, text, x, y, color, effect, effectSpeed, timeSec, false); + if (!rendered) renderer.render(text, x, y, new RenderColor(color), false); + float width = (float) renderer.getWidth(text, false); + renderer.end(); + return x + width; + } + + static void renderDecorations(Renderer2D renderer, float x, float y, float w, float lineHeight, Part part) { + if (renderer == null || part == null || w <= 0.0f) return; + if (!part.underline() && !part.strikethrough()) return; + int color = part.color(); + float h = Math.max(0.65f, lineHeight * 0.055f); + if (part.underline()) renderer.quad(x, y + lineHeight * 0.88f, w, h, color); + if (part.strikethrough()) renderer.quad(x, y + lineHeight * 0.54f, w, h, color); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/scripted/ScriptedInventoryHudPanel.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/scripted/ScriptedInventoryHudPanel.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/scripted/ScriptedInventoryHudPanel.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/scripted/ScriptedInventoryHudPanel.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/scripted/ScriptedListHudPanel.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/scripted/ScriptedListHudPanel.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/scripted/ScriptedListHudPanel.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/scripted/ScriptedListHudPanel.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/scripted/ScriptedTriangulatorHudPanel.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/scripted/ScriptedTriangulatorHudPanel.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/scripted/ScriptedTriangulatorHudPanel.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/scripted/ScriptedTriangulatorHudPanel.java diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Fps.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Fps.java new file mode 100644 index 0000000..21c149e --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Fps.java @@ -0,0 +1,168 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.stats; + +import dev.loki.lovisual.config.values.color.RGBAColorValue; +import dev.loki.lovisual.config.values.color.RGBColorValue; +import dev.loki.lovisual.config.values.mode.EnumValue; +import dev.loki.lovisual.config.values.mode.ModeValue; +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.resources.Identifier; +import dev.loki.lovisual.features.gui.hud.registry.HudElementInfo; +import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; +import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; +import dev.loki.lovisual.features.gui.hud.script.CompactHudStatModel; +import dev.loki.lovisual.features.gui.hud.script.ScriptableHudStatWidget; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; + +@HudElementInfo( + id = "fps", + displayName = "FPS", + enabledByDefault = true, + order = 120 +) +public final class Fps extends DraggableHudElement implements ScriptableHudStatWidget { + + static final Identifier FPS_ICON = Identifier.fromNamespaceAndPath("lovisual", "textures/hud/elements/fps.png"); + static final float SCALE_MULT = 0.68f; + static final float BOX_HEIGHT = 20f; + static final float BOX_RADIUS = 5f; + static final float BOX_SOFTNESS = 1.0f; + static final float STROKE_WIDTH = 0.55f; + static final float ICON_SIZE = 12.5f; + static final float ICON_X = 5f; + static final float DIVIDER_X = 21f; + static final float DIVIDER_Y = 4f; + static final float DIVIDER_WIDTH = 0.6f; + static final float TEXT_LEFT_X = 26f; + static final float VALUE_UNIT_GAP = 2f; + static final float RIGHT_PAD = 5f; + static final String COLOR_THEME = "Theme"; + static final String COLOR_CUSTOM = "Custom"; + static final String EFFECT_NONE = "None"; + static final String EFFECT_BLUR = "Blur"; + static final String EFFECT_GLASS = "Glass"; + static final long DIGIT_ANIMATION_DURATION_MS = 200L; + static final float DIGIT_ANIMATION_OFFSET = 8.0f; + + final HudGlobalConfig hud = HudGlobalConfig.get(); + final NumberValue scale = num("fps_scale", 2.37, 0.5, 5.0); + final ModeValue colorMode = mode("fps_color_mode", "color_mode", "Theme", new String[]{COLOR_THEME, COLOR_CUSTOM}); + final RGBColorValue iconColor = visibleWhen(colorNoAlpha("fps_icon_color", "#FFFFFF"), this::isCustomMode); + final RGBColorValue valueColor = visibleWhen(colorNoAlpha("fps_value_color", "#FFFFFF"), this::isCustomMode); + final RGBColorValue metaColor = visibleWhen(colorNoAlpha("fps_meta_color", "#9B9B9B"), this::isCustomMode); + final ModeValue bgEffect = mode("fps_bg_effect", "bg_effect", "Blur", new String[]{EFFECT_NONE, EFFECT_BLUR, EFFECT_GLASS}); + final RGBAColorValue bg = visibleWhen(color("fps_bg", "#F7343434"), () -> isCustomMode() && !isGlassEffect()); + final RGBAColorValue bg2 = visibleWhen(color("fps_bg_secondary", "#F7161616"), () -> isCustomMode() && !isGlassEffect()); + final RGBColorValue stroke = visibleWhen(colorNoAlpha("fps_stroke", "#5A5A5A"), () -> isCustomMode() && !isGlassEffect()); + final NumberValue bgAlpha = visibleWhen(num("fps_bg_alpha", 133, 0, 255), () -> isThemeMode() && !isGlassEffect()); + final NumberValue blurAlpha = visibleWhen(num("fps_blur_alpha", 255, 0, 255), this::hasEffect); + final EnumValue labelEffect = + enumSetting("fps_label_effect", HudTextEffects.Effect.FLOW, + HudTextEffects.Effect.NONE, HudTextEffects.Effect.MIX, HudTextEffects.Effect.FLOW, + HudTextEffects.Effect.PULSE, HudTextEffects.Effect.STRIPE); + final NumberValue labelEffectSpeed = + visibleWhen(num("fps_label_effect_speed", 18, 1, 60), + () -> labelEffect.get() != HudTextEffects.Effect.NONE); + final BooleanValue digitAnimation = bool("fps_digit_animation", false); + final NumberValue digitPollMs = + visibleWhen(num("fps_digit_poll_ms", 495, 50, 1000), digitAnimation::get); + final CompactHudStatModel scriptModel = new CompactHudStatModel("fps"); + + String shownFps = "0"; + String previousShownFps = ""; + long digitAnimStartMs = 0L; + long lastDigitPollMs = 0L; + float displayValueWidth = -1f; + + int uiBgPrimary; + int uiBgSecondary; + int uiStroke; + int uiIconColor; + int uiValueColor; + int uiMetaColor; + + @Override + public void applyDefaultPosition(int screenW, int screenH) { + this.x = 16f; + this.y = 20f; + } + + @Override + public boolean usesEngineRenderer() { + return true; + } + + @Override + public void renderEngine(Renderer2D renderer, + TextRenderer textRenderer, + GuiGraphicsExtractor ctx, + float tickDelta, + int screenW, + int screenH) { + boolean preview = DraggableHudElementRegistry.isForceVisible(); + if (Minecraft.getInstance() == null && !preview) { + width = 0f; + height = 0f; + scriptModel.setVisible(false); + return; + } + if (!preview && !isEnabled()) { + width = 0f; + height = 0f; + scriptModel.setVisible(false); + return; + } + FpsRender.render(this, renderer, textRenderer, ctx, tickDelta, screenW, screenH); + } + + @Override + public boolean supportsWidgetAnchoring() { + return true; + } + + @Override + public CompactHudStatModel compactStatModel() { + return scriptModel; + } + + boolean isThemeMode() { + return COLOR_THEME.equals(colorMode.get()); + } + + boolean isCustomMode() { + return COLOR_CUSTOM.equals(colorMode.get()); + } + + boolean isGlassEffect() { + return EFFECT_GLASS.equals(bgEffect.get()); + } + + boolean hasEffect() { + return !EFFECT_NONE.equals(bgEffect.get()); + } + + public float hudX() { + return getX(); + } + + public float hudY() { + return getY(); + } + + public void setHudSize(float w, float h) { + this.width = w; + this.height = h; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Memory.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Memory.java new file mode 100644 index 0000000..f838d31 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Memory.java @@ -0,0 +1,151 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.stats; + +import dev.loki.lovisual.config.values.color.RGBAColorValue; +import dev.loki.lovisual.config.values.color.RGBColorValue; +import dev.loki.lovisual.config.values.mode.EnumValue; +import dev.loki.lovisual.config.values.mode.ModeValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.resources.Identifier; +import dev.loki.lovisual.features.gui.hud.registry.HudElementInfo; +import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; +import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; +import dev.loki.lovisual.features.gui.hud.script.CompactHudStatModel; +import dev.loki.lovisual.features.gui.hud.script.ScriptableHudStatWidget; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; + +@HudElementInfo( + id = "memory", + displayName = "Memory", + enabledByDefault = true, + order = 130 +) +public final class Memory extends DraggableHudElement implements ScriptableHudStatWidget { + + static final Identifier MEMORY_ICON = Identifier.fromNamespaceAndPath("lovisual", "textures/hud/elements/memory.png"); + static final float SCALE_MULT = 0.68f; + static final float BOX_HEIGHT = 20f; + static final float BOX_RADIUS = 5f; + static final float BOX_SOFTNESS = 1.0f; + static final float STROKE_WIDTH = 0.55f; + static final float ICON_SIZE = 11f; + static final float ICON_X = 5f; + static final float DIVIDER_X = 21f; + static final float DIVIDER_Y = 4f; + static final float DIVIDER_WIDTH = 0.6f; + static final float TEXT_LEFT_X = 26f; + static final float VALUE_UNIT_GAP = 2f; + static final float EXTRA_GAP = 5f; + static final float RIGHT_PAD = 5f; + static final String COLOR_THEME = "Theme"; + static final String COLOR_CUSTOM = "Custom"; + static final String EFFECT_NONE = "None"; + static final String EFFECT_BLUR = "Blur"; + static final String EFFECT_GLASS = "Glass"; + + final HudGlobalConfig hud = HudGlobalConfig.get(); + final NumberValue scale = num("memory_scale", 2.37, 0.5, 5.0); + final ModeValue colorMode = mode("memory_color_mode", "color_mode", "Theme", new String[]{COLOR_THEME, COLOR_CUSTOM}); + final RGBColorValue iconColor = visibleWhen(colorNoAlpha("memory_icon_color", "#FFFFFF"), this::isCustomMode); + final RGBColorValue valueColor = visibleWhen(colorNoAlpha("memory_value_color", "#FFFFFF"), this::isCustomMode); + final RGBColorValue metaColor = visibleWhen(colorNoAlpha("memory_meta_color", "#9B9B9B"), this::isCustomMode); + final RGBColorValue extraColor = visibleWhen(colorNoAlpha("memory_extra_color", "#BEBEBE"), this::isCustomMode); + final ModeValue bgEffect = mode("memory_bg_effect", "bg_effect", "Blur", new String[]{EFFECT_NONE, EFFECT_BLUR, EFFECT_GLASS}); + final RGBAColorValue bg = visibleWhen(color("memory_bg", "#F7343434"), () -> isCustomMode() && !isGlassEffect()); + final RGBAColorValue bg2 = visibleWhen(color("memory_bg_secondary", "#F7161616"), () -> isCustomMode() && !isGlassEffect()); + final RGBColorValue stroke = visibleWhen(colorNoAlpha("memory_stroke", "#5A5A5A"), () -> isCustomMode() && !isGlassEffect()); + final NumberValue bgAlpha = visibleWhen(num("memory_bg_alpha", 135, 0, 255), () -> isThemeMode() && !isGlassEffect()); + final NumberValue blurAlpha = visibleWhen(num("memory_blur_alpha", 255, 0, 255), this::hasEffect); + final EnumValue labelEffect = + enumSetting("memory_label_effect", HudTextEffects.Effect.NONE, + HudTextEffects.Effect.NONE, HudTextEffects.Effect.MIX, HudTextEffects.Effect.FLOW, + HudTextEffects.Effect.PULSE, HudTextEffects.Effect.STRIPE); + final NumberValue labelEffectSpeed = + visibleWhen(num("memory_label_effect_speed", 18, 1, 60), + () -> labelEffect.get() != HudTextEffects.Effect.NONE); + final CompactHudStatModel scriptModel = new CompactHudStatModel("memory"); + final StringBuilder valueBuilder = new StringBuilder(24); + long lastUsedMiB = Long.MIN_VALUE; + long lastMaxMiB = Long.MIN_VALUE; + int lastPercent = Integer.MIN_VALUE; + String cachedValue = "0/0"; + String cachedExtra = "[0%]"; + float displayValueWidth = -1f; + float displayExtraWidth = -1f; + int uiBgPrimary; + int uiBgSecondary; + int uiStroke; + int uiIconColor; + int uiValueColor; + int uiMetaColor; + int uiExtraColor; + + @Override + public void applyDefaultPosition(int screenW, int screenH) { + this.x = 16f; + this.y = 44f; + } + + @Override + public boolean usesEngineRenderer() { + return true; + } + + @Override + public void renderEngine(Renderer2D renderer, + TextRenderer textRenderer, + GuiGraphicsExtractor ctx, + float tickDelta, + int screenW, + int screenH) { + MemoryRender.render(this, renderer, textRenderer, ctx, tickDelta, screenW, screenH); + } + + @Override + public boolean supportsWidgetAnchoring() { + return true; + } + + @Override + public CompactHudStatModel compactStatModel() { + return scriptModel; + } + + boolean isThemeMode() { + return COLOR_THEME.equals(colorMode.get()); + } + + boolean isCustomMode() { + return COLOR_CUSTOM.equals(colorMode.get()); + } + + boolean isGlassEffect() { + return EFFECT_GLASS.equals(bgEffect.get()); + } + + boolean hasEffect() { + return !EFFECT_NONE.equals(bgEffect.get()); + } + + public float hudX() { + return getX(); + } + + public float hudY() { + return getY(); + } + + public void setHudSize(float w, float h) { + this.width = w; + this.height = h; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Ping.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Ping.java new file mode 100644 index 0000000..00c1ac8 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Ping.java @@ -0,0 +1,145 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.stats; + +import dev.loki.lovisual.config.values.color.RGBAColorValue; +import dev.loki.lovisual.config.values.color.RGBColorValue; +import dev.loki.lovisual.config.values.mode.EnumValue; +import dev.loki.lovisual.config.values.mode.ModeValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.resources.Identifier; +import dev.loki.lovisual.features.gui.hud.registry.HudElementInfo; +import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; +import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; +import dev.loki.lovisual.features.gui.hud.script.CompactHudStatModel; +import dev.loki.lovisual.features.gui.hud.script.ScriptableHudStatWidget; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; + +//todo Description +@HudElementInfo( + id = "ping", + displayName = "Ping", + enabledByDefault = false, + order = 150 +) +public final class Ping extends DraggableHudElement implements ScriptableHudStatWidget { + + static final Identifier PING_ICON = Identifier.fromNamespaceAndPath("lovisual", "textures/hud/elements/ping.png"); + static final float SCALE_MULT = 0.68f; + static final float BOX_HEIGHT = 20f; + static final float BOX_RADIUS = 5f; + static final float BOX_SOFTNESS = 1.0f; + static final float STROKE_WIDTH = 0.55f; + static final float ICON_SIZE = 11f; + static final float ICON_X = 5f; + static final float DIVIDER_X = 21f; + static final float DIVIDER_Y = 4f; + static final float DIVIDER_WIDTH = 0.6f; + static final float TEXT_LEFT_X = 26f; + static final float VALUE_UNIT_GAP = 2f; + static final float RIGHT_PAD = 5f; + static final String COLOR_THEME = "Theme"; + static final String COLOR_CUSTOM = "Custom"; + static final String EFFECT_NONE = "None"; + static final String EFFECT_BLUR = "Blur"; + static final String EFFECT_GLASS = "Glass"; + + final HudGlobalConfig hud = HudGlobalConfig.get(); + final NumberValue scale = num("ping_scale", 2.58, 0.5, 5.0); + final ModeValue colorMode = mode("ping_color_mode", "color_mode", "Theme", new String[]{COLOR_THEME, COLOR_CUSTOM}); + final RGBColorValue iconColor = visibleWhen(colorNoAlpha("ping_icon_color", "#FFFFFF"), this::isCustomMode); + final RGBColorValue valueColor = visibleWhen(colorNoAlpha("ping_value_color", "#FFFFFF"), this::isCustomMode); + final RGBColorValue metaColor = visibleWhen(colorNoAlpha("ping_meta_color", "#9B9B9B"), this::isCustomMode); + final ModeValue bgEffect = mode("ping_bg_effect", "bg_effect", "None", new String[]{EFFECT_NONE, EFFECT_BLUR, EFFECT_GLASS}); + final RGBAColorValue bg = visibleWhen(color("ping_bg", "#F7343434"), () -> isCustomMode() && !isGlassEffect()); + final RGBAColorValue bg2 = visibleWhen(color("ping_bg_secondary", "#F7161616"), () -> isCustomMode() && !isGlassEffect()); + final RGBColorValue stroke = visibleWhen(colorNoAlpha("ping_stroke", "#5A5A5A"), () -> isCustomMode() && !isGlassEffect()); + final NumberValue bgAlpha = visibleWhen(num("ping_bg_alpha", 225, 0, 255), () -> isThemeMode() && !isGlassEffect()); + final NumberValue blurAlpha = visibleWhen(num("ping_blur_alpha", 140, 0, 255), this::hasEffect); + final EnumValue labelEffect = + enumSetting("ping_label_effect", HudTextEffects.Effect.NONE, + HudTextEffects.Effect.NONE, HudTextEffects.Effect.MIX, HudTextEffects.Effect.FLOW, + HudTextEffects.Effect.PULSE, HudTextEffects.Effect.STRIPE); + final NumberValue labelEffectSpeed = + visibleWhen(num("ping_label_effect_speed", 18, 1, 60), + () -> labelEffect.get() != HudTextEffects.Effect.NONE); + final CompactHudStatModel scriptModel = new CompactHudStatModel("ping"); + + int lastPing = Integer.MIN_VALUE; + String cachedValue = "--"; + float displayValueWidth = -1f; + int uiBgPrimary; + int uiBgSecondary; + int uiStroke; + int uiIconColor; + int uiValueColor; + int uiMetaColor; + + @Override + public void applyDefaultPosition(int screenW, int screenH) { + this.x = 16f; + this.y = 76f; + } + + @Override + public boolean usesEngineRenderer() { + return true; + } + + @Override + public void renderEngine(Renderer2D renderer, + TextRenderer textRenderer, + GuiGraphicsExtractor ctx, + float tickDelta, + int screenW, + int screenH) { + PingRender.render(this, renderer, textRenderer, ctx, tickDelta, screenW, screenH); + } + + @Override + public boolean supportsWidgetAnchoring() { + return true; + } + + @Override + public CompactHudStatModel compactStatModel() { + return scriptModel; + } + + boolean isThemeMode() { + return COLOR_THEME.equals(colorMode.get()); + } + + boolean isCustomMode() { + return COLOR_CUSTOM.equals(colorMode.get()); + } + + boolean isGlassEffect() { + return EFFECT_GLASS.equals(bgEffect.get()); + } + + boolean hasEffect() { + return !EFFECT_NONE.equals(bgEffect.get()); + } + + public float hudX() { + return getX(); + } + + public float hudY() { + return getY(); + } + + public void setHudSize(float w, float h) { + this.width = w; + this.height = h; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Tps.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Tps.java new file mode 100644 index 0000000..23bd6bd --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/Tps.java @@ -0,0 +1,154 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.stats; + +import dev.loki.lovisual.config.values.color.RGBAColorValue; +import dev.loki.lovisual.config.values.color.RGBColorValue; +import dev.loki.lovisual.config.values.mode.EnumValue; +import dev.loki.lovisual.config.values.mode.ModeValue; +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.resources.Identifier; +import dev.loki.lovisual.features.gui.hud.registry.HudElementInfo; +import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; +import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; +import dev.loki.lovisual.features.gui.hud.script.CompactHudStatModel; +import dev.loki.lovisual.features.gui.hud.script.ScriptableHudStatWidget; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; + +@HudElementInfo( + id = "tps", + displayName = "TPS", + enabledByDefault = false, + order = 160 +) +public final class Tps extends DraggableHudElement implements ScriptableHudStatWidget { + + static final Identifier TPS_ICON = Identifier.fromNamespaceAndPath("lovisual", "textures/hud/elements/tps.png"); + static final float SCALE_MULT = 0.68f; + static final float BOX_HEIGHT = 20f; + static final float BOX_RADIUS = 5f; + static final float BOX_SOFTNESS = 1.0f; + static final float STROKE_WIDTH = 0.55f; + static final float ICON_SIZE = 11f; + static final float ICON_X = 5f; + static final float DIVIDER_X = 21f; + static final float DIVIDER_Y = 4f; + static final float DIVIDER_WIDTH = 0.6f; + static final float TEXT_LEFT_X = 26f; + static final float VALUE_UNIT_GAP = 2f; + static final float EXTRA_GAP = 5f; + static final float RIGHT_PAD = 5f; + static final String COLOR_THEME = "Theme"; + static final String COLOR_CUSTOM = "Custom"; + static final String EFFECT_NONE = "None"; + static final String EFFECT_BLUR = "Blur"; + static final String EFFECT_GLASS = "Glass"; + + final HudGlobalConfig hud = HudGlobalConfig.get(); + final BooleanValue extra = bool("tps_extra", false); + final NumberValue scale = num("tps_scale", 2.37, 0.5, 5.0); + final ModeValue colorMode = mode("tps_color_mode", "color_mode", "Theme", new String[]{COLOR_THEME, COLOR_CUSTOM}); + final RGBColorValue iconColor = visibleWhen(colorNoAlpha("tps_icon_color", "#FFFFFF"), this::isCustomMode); + final RGBColorValue valueColor = visibleWhen(colorNoAlpha("tps_value_color", "#FFFFFF"), this::isCustomMode); + final RGBColorValue metaColor = visibleWhen(colorNoAlpha("tps_meta_color", "#9B9B9B"), this::isCustomMode); + final RGBColorValue extraColor = visibleWhen(colorNoAlpha("tps_extra_color", "#BEBEBE"), this::isCustomMode); + final ModeValue bgEffect = mode("tps_bg_effect", "bg_effect", "Blur", new String[]{EFFECT_NONE, EFFECT_BLUR, EFFECT_GLASS}); + final RGBAColorValue bg = visibleWhen(color("tps_bg", "#F7343434"), () -> isCustomMode() && !isGlassEffect()); + final RGBAColorValue bg2 = visibleWhen(color("tps_bg_secondary", "#F7161616"), () -> isCustomMode() && !isGlassEffect()); + final RGBColorValue stroke = visibleWhen(colorNoAlpha("tps_stroke", "#5A5A5A"), () -> isCustomMode() && !isGlassEffect()); + final NumberValue bgAlpha = visibleWhen(num("tps_bg_alpha", 133, 0, 255), () -> isThemeMode() && !isGlassEffect()); + final NumberValue blurAlpha = visibleWhen(num("tps_blur_alpha", 255, 0, 255), this::hasEffect); + final EnumValue labelEffect = + enumSetting("tps_label_effect", HudTextEffects.Effect.NONE, + HudTextEffects.Effect.NONE, HudTextEffects.Effect.MIX, HudTextEffects.Effect.FLOW, + HudTextEffects.Effect.PULSE, HudTextEffects.Effect.STRIPE); + final NumberValue labelEffectSpeed = + visibleWhen(num("tps_label_effect_speed", 18, 1, 60), + () -> labelEffect.get() != HudTextEffects.Effect.NONE); + final CompactHudStatModel scriptModel = new CompactHudStatModel("tps"); + final StringBuilder tpsBuilder = new StringBuilder(16); + final StringBuilder extraBuilder = new StringBuilder(16); + int lastTpsTenth = Integer.MIN_VALUE; + int lastExtraHundredth = Integer.MIN_VALUE; + boolean lastExtra; + String cachedValue = "20.0"; + String cachedExtra = ""; + float displayValueWidth = -1f; + float displayExtraWidth = -1f; + int uiBgPrimary; + int uiBgSecondary; + int uiStroke; + int uiIconColor; + int uiValueColor; + int uiMetaColor; + int uiExtraColor; + + @Override + public void applyDefaultPosition(int screenW, int screenH) { + this.x = 16f; + this.y = 60f; + } + + @Override + public boolean usesEngineRenderer() { + return true; + } + + @Override + public void renderEngine(Renderer2D renderer, + TextRenderer textRenderer, + GuiGraphicsExtractor ctx, + float tickDelta, + int screenW, + int screenH) { + TpsRender.render(this, renderer, textRenderer, ctx, tickDelta, screenW, screenH); + } + + @Override + public boolean supportsWidgetAnchoring() { + return true; + } + + @Override + public CompactHudStatModel compactStatModel() { + return scriptModel; + } + + boolean isThemeMode() { + return COLOR_THEME.equals(colorMode.get()); + } + + boolean isCustomMode() { + return COLOR_CUSTOM.equals(colorMode.get()); + } + + boolean isGlassEffect() { + return EFFECT_GLASS.equals(bgEffect.get()); + } + + boolean hasEffect() { + return !EFFECT_NONE.equals(bgEffect.get()); + } + + public float hudX() { + return getX(); + } + + public float hudY() { + return getY(); + } + + public void setHudSize(float w, float h) { + this.width = w; + this.height = h; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/fps/FpsAnimator.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/fps/FpsAnimator.java new file mode 100644 index 0000000..6ca07a4 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/fps/FpsAnimator.java @@ -0,0 +1,66 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.stats; + +import dev.loki.lovisual.render.engine.animation.AnimationUtility; + +final class FpsAnimator { + + private FpsAnimator() { + } + + static String widerText(String a, String b) { + return a != null && b != null && a.length() < b.length() ? b : a; + } + + static float smoothWidth(float current, float target) { + if (current < 0.0f) return target; + if (target >= current) return target; + float next = AnimationUtility.approach(current, target, AnimationUtility.deltaTime(), 12.0f); + return AnimationUtility.snap(next, target, 0.25f); + } + + static String resolveDisplayedFps(Fps host, String rawValue, long nowMs) { + if (!host.digitAnimation.get()) { + host.shownFps = rawValue; + host.previousShownFps = rawValue; + host.digitAnimStartMs = 0L; + host.lastDigitPollMs = nowMs; + return rawValue; + } + + if (host.shownFps == null || host.shownFps.isEmpty()) { + host.shownFps = rawValue; + host.previousShownFps = rawValue; + host.digitAnimStartMs = 0L; + host.lastDigitPollMs = nowMs; + return host.shownFps; + } + + if (nowMs - host.lastDigitPollMs < host.digitPollMs.get()) { + return host.shownFps; + } + + host.lastDigitPollMs = nowMs; + if (!rawValue.equals(host.shownFps)) { + host.previousShownFps = host.shownFps; + host.shownFps = rawValue; + host.digitAnimStartMs = nowMs; + } + return host.shownFps; + } + + static float digitProgress(Fps host, long nowMs) { + if (!host.digitAnimation.get() || host.digitAnimStartMs <= 0L) { + return 1.0f; + } + return AnimationUtility.clamp01( + (nowMs - host.digitAnimStartMs) / (float) Fps.DIGIT_ANIMATION_DURATION_MS + ); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/fps/FpsPalette.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/fps/FpsPalette.java new file mode 100644 index 0000000..95d8d45 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/fps/FpsPalette.java @@ -0,0 +1,60 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.stats; + +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; + +import static dev.loki.lovisual.features.theme.Theme.theme; + +final class FpsPalette { + + private FpsPalette() { + } + + static void update(Fps host) { + if (isThemeMode(host)) { + int panelAlpha = host.bgAlpha.get(); + host.uiBgPrimary = HudRenderUtil.setAlpha(theme().windowBg(), panelAlpha); + host.uiBgSecondary = HudRenderUtil.setAlpha( + HudRenderUtil.mixColor(theme().surface(), theme().windowHeader(), 0.35f), + panelAlpha + ); + host.uiStroke = HudRenderUtil.setAlpha( + HudRenderUtil.mixColor(theme().windowStroke(), theme().strokeSoft(), 0.4f), + Math.min(panelAlpha, 190) + ); + host.uiIconColor = theme().textPrimary(); + host.uiValueColor = theme().textPrimary(); + host.uiMetaColor = theme().textMuted(); + return; + } + + host.uiBgPrimary = host.bg.getArgb(); + host.uiBgSecondary = host.bg2.getArgb(); + host.uiStroke = host.stroke.getArgb(); + host.uiIconColor = host.iconColor.getArgb(); + host.uiValueColor = host.valueColor.getArgb(); + host.uiMetaColor = host.metaColor.getArgb(); + } + + static boolean isThemeMode(Fps host) { + return Fps.COLOR_THEME.equals(host.colorMode.get()); + } + + static boolean isCustomMode(Fps host) { + return Fps.COLOR_CUSTOM.equals(host.colorMode.get()); + } + + static boolean isGlassEffect(Fps host) { + return Fps.EFFECT_GLASS.equals(host.bgEffect.get()); + } + + static boolean hasEffect(Fps host) { + return !Fps.EFFECT_NONE.equals(host.bgEffect.get()); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/fps/FpsRender.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/fps/FpsRender.java new file mode 100644 index 0000000..bdb1e17 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/fps/FpsRender.java @@ -0,0 +1,107 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.stats; + +import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; +import dev.loki.lovisual.features.gui.hud.script.ScriptedCompactHudStatRenderer; +import dev.loki.lovisual.render.engine.math.HudScale; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.font.Fonts; +import dev.loki.lovisual.render.engine.text.font.info.FontInfo; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.util.FastFps; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.resources.Identifier; +import net.minecraft.util.Util; + +final class FpsRender { + + private FpsRender() { + } + + static void render(Fps host, + Renderer2D renderer, + TextRenderer textRenderer, + GuiGraphicsExtractor ctx, + float tickDelta, + int screenW, + int screenH) { + int fps = Math.max(0, FastFps.getFps()); + long nowMs = Util.getMillis(); + String value = FpsAnimator.resolveDisplayedFps(host, Integer.toString(fps), nowMs); + String unit = "FPS"; + float digitAnimProgress = FpsAnimator.digitProgress(host, nowMs); + String widthValue = host.digitAnimation.get() ? FpsAnimator.widerText(value, host.previousShownFps) : value; + FpsPalette.update(host); + + TextRenderer fallback = textRenderer != null ? textRenderer : TextRenderer.get(); + TextRenderer valueRenderer = Fonts.renderer("Onest", FontInfo.Type.Regular, fallback); + TextRenderer metaRenderer = Fonts.renderer("OnestMedium", FontInfo.Type.Regular, valueRenderer); + + float drawScale = HudScale.scale(screenW, screenH) + * (host.hud.getFontSize() / 18f) + * Fps.SCALE_MULT + * host.scale.get().floatValue(); + + valueRenderer.begin(drawScale, true, false); + float valueW = (float) valueRenderer.getWidth(widthValue, false); + float valueH = (float) valueRenderer.getHeight(false); + valueRenderer.end(); + + metaRenderer.begin(drawScale, true, false); + float unitW = (float) metaRenderer.getWidth(unit, false); + float unitH = (float) metaRenderer.getHeight(false); + metaRenderer.end(); + + float boxH = Fps.BOX_HEIGHT * drawScale; + float iconSize = Fps.ICON_SIZE * drawScale; + float valueX = host.hudX() + Fps.TEXT_LEFT_X * drawScale; + float unitGap = Fps.VALUE_UNIT_GAP * drawScale; + host.displayValueWidth = FpsAnimator.smoothWidth(host.displayValueWidth, valueW); + float boxW = (valueX - host.hudX()) + host.displayValueWidth + unitGap + unitW + Fps.RIGHT_PAD * drawScale; + host.setHudSize(boxW, boxH); + + float baseX = host.hudX(); + float baseY = host.hudY(); + float iconX = baseX + Fps.ICON_X * drawScale; + float iconY = baseY + (boxH - iconSize) * 0.5f; + float dividerX = baseX + Fps.DIVIDER_X * drawScale; + float dividerY = baseY + Fps.DIVIDER_Y * drawScale; + float dividerW = Math.max(0.5f, Fps.DIVIDER_WIDTH * drawScale); + float dividerH = Math.max(0.5f, boxH - 2f * Fps.DIVIDER_Y * drawScale); + float textY = baseY + (boxH - Math.max(valueH, unitH)) * 0.5f; + float unitX = valueX + host.displayValueWidth + unitGap; + + int resolvedIconColor = host.uiIconColor; + boolean useLabelEffect = host.labelEffect.get() != HudTextEffects.Effect.NONE; + float time = useLabelEffect ? (float) (Util.getMillis() / 1000.0) : 0.0f; + if (useLabelEffect) { + resolvedIconColor = HudTextEffects.animatedColor(resolvedIconColor, host.labelEffect.get(), + host.labelEffectSpeed.get(), time, 0.0f); + } + + float radius = Fps.BOX_RADIUS * drawScale; + host.scriptModel.setVisible(true); + host.scriptModel.setRoot(baseX, baseY, boxW, boxH, radius, drawScale); + host.scriptModel.background().set(host.bgEffect.get(), FpsPalette.isThemeMode(host), host.blurAlpha.get() / 255.0f, + host.uiBgPrimary, host.uiBgSecondary, host.uiStroke, Math.max(0.5f, Fps.STROKE_WIDTH * drawScale), Fps.BOX_SOFTNESS); + host.scriptModel.icon().texture(Fps.FPS_ICON.toString(), iconX - baseX, iconY - baseY, iconSize, iconSize, resolvedIconColor); + host.scriptModel.divider().set(dividerX - baseX, dividerY - baseY, dividerW, dividerH, + HudRenderUtil.setAlpha(host.uiMetaColor, 0x52)); + host.scriptModel.value().set(value, "Onest", drawScale, valueX - baseX, textY - baseY, host.displayValueWidth, host.uiValueColor); + host.scriptModel.unit().set(unit, "OnestMedium", drawScale, unitX - baseX, textY - baseY, unitW, host.uiMetaColor); + host.scriptModel.extra().hidden(); + host.scriptModel.animation().setLabelEffect(host.labelEffect.get().name(), host.labelEffectSpeed.get(), time) + .setDigitAnimation(host.digitAnimation.get(), host.previousShownFps, digitAnimProgress, Fps.DIGIT_ANIMATION_OFFSET); + host.scriptModel.data("fps", fps) + .data("valueText", value) + .data("unitText", unit); + ScriptedCompactHudStatRenderer.INSTANCE.render(host.scriptModel, renderer, fallback, ctx, tickDelta); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/memory/MemoryFormat.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/memory/MemoryFormat.java new file mode 100644 index 0000000..c85af21 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/memory/MemoryFormat.java @@ -0,0 +1,32 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.stats; + +final class MemoryFormat { + + private MemoryFormat() { + } + + static long toMiB(long bytes) { + return bytes / 1024L / 1024L; + } + + static void updateMemoryStrings(Memory host, long usedMiB, long maxMiB, int percent) { + if (usedMiB == host.lastUsedMiB && maxMiB == host.lastMaxMiB && percent == host.lastPercent) { + return; + } + host.lastUsedMiB = usedMiB; + host.lastMaxMiB = maxMiB; + host.lastPercent = percent; + + host.valueBuilder.setLength(0); + host.valueBuilder.append(usedMiB).append('/').append(maxMiB); + host.cachedValue = host.valueBuilder.toString(); + host.cachedExtra = "[" + percent + "%]"; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/memory/MemoryPalette.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/memory/MemoryPalette.java new file mode 100644 index 0000000..f06f0e1 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/memory/MemoryPalette.java @@ -0,0 +1,46 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.stats; + +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; + +import static dev.loki.lovisual.features.theme.Theme.theme; + +final class MemoryPalette { + + private MemoryPalette() { + } + + static void update(Memory host) { + if (host.isThemeMode()) { + int panelAlpha = host.bgAlpha.get(); + host.uiBgPrimary = HudRenderUtil.setAlpha(theme().windowBg(), panelAlpha); + host.uiBgSecondary = HudRenderUtil.setAlpha( + HudRenderUtil.mixColor(theme().surface(), theme().windowHeader(), 0.35f), + panelAlpha + ); + host.uiStroke = HudRenderUtil.setAlpha( + HudRenderUtil.mixColor(theme().windowStroke(), theme().strokeSoft(), 0.4f), + Math.min(panelAlpha, 190) + ); + host.uiIconColor = theme().textPrimary(); + host.uiValueColor = theme().textPrimary(); + host.uiMetaColor = theme().textMuted(); + host.uiExtraColor = HudRenderUtil.mixColor(theme().textMuted(), theme().textPrimary(), 0.18f); + return; + } + + host.uiBgPrimary = host.bg.getArgb(); + host.uiBgSecondary = host.bg2.getArgb(); + host.uiStroke = host.stroke.getArgb(); + host.uiIconColor = host.iconColor.getArgb(); + host.uiValueColor = host.valueColor.getArgb(); + host.uiMetaColor = host.metaColor.getArgb(); + host.uiExtraColor = host.extraColor.getArgb(); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/memory/MemoryRender.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/memory/MemoryRender.java new file mode 100644 index 0000000..c5ab96d --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/memory/MemoryRender.java @@ -0,0 +1,144 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.stats; + +import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; +import dev.loki.lovisual.features.gui.hud.script.ScriptedCompactHudStatRenderer; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.math.HudScale; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.font.Fonts; +import dev.loki.lovisual.render.engine.text.font.info.FontInfo; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.util.Util; + +final class MemoryRender { + + private MemoryRender() { + } + + static void render(Memory host, + Renderer2D renderer, + TextRenderer textRenderer, + GuiGraphicsExtractor ctx, + float tickDelta, + int screenW, + int screenH) { + boolean preview = DraggableHudElementRegistry.isForceVisible(); + if (Minecraft.getInstance() == null && !preview) { + host.setHudSize(0f, 0f); + host.scriptModel.setVisible(false); + return; + } + if (!preview && !host.isEnabled()) { + host.setHudSize(0f, 0f); + host.scriptModel.setVisible(false); + return; + } + + Runtime rt = Runtime.getRuntime(); + long max = rt.maxMemory(); + long total = rt.totalMemory(); + long free = rt.freeMemory(); + long used = Math.max(0L, total - free); + long usedMiB = MemoryFormat.toMiB(used); + long maxMiB = MemoryFormat.toMiB(max > 0L ? max : total); + int percent = max > 0L ? (int) Math.round((double) used * 100.0 / (double) max) : 0; + percent = Math.max(0, Math.min(100, percent)); + MemoryFormat.updateMemoryStrings(host, usedMiB, maxMiB, percent); + MemoryPalette.update(host); + + TextRenderer fallback = textRenderer != null ? textRenderer : TextRenderer.get(); + TextRenderer valueRenderer = Fonts.renderer("Onest", FontInfo.Type.Regular, fallback); + TextRenderer metaRenderer = Fonts.renderer("OnestMedium", FontInfo.Type.Regular, valueRenderer); + + float drawScale = HudScale.scale(screenW, screenH) + * (host.hud.getFontSize() / 18f) + * Memory.SCALE_MULT + * host.scale.get().floatValue(); + + valueRenderer.begin(drawScale, true, false); + float valueW = (float) valueRenderer.getWidth(host.cachedValue, false); + float valueH = (float) valueRenderer.getHeight(false); + valueRenderer.end(); + + metaRenderer.begin(drawScale, true, false); + float unitW = (float) metaRenderer.getWidth("MB", false); + float unitH = (float) metaRenderer.getHeight(false); + float extraW = (float) metaRenderer.getWidth(host.cachedExtra, false); + metaRenderer.end(); + + float boxH = Memory.BOX_HEIGHT * drawScale; + float iconSize = Memory.ICON_SIZE * drawScale; + float valueX = host.hudX() + Memory.TEXT_LEFT_X * drawScale; + float unitGap = Memory.VALUE_UNIT_GAP * drawScale; + float extraGap = Memory.EXTRA_GAP * drawScale; + host.displayValueWidth = smoothWidth(host.displayValueWidth, valueW); + host.displayExtraWidth = smoothWidth(host.displayExtraWidth, extraW); + float boxW = (valueX - host.hudX()) + + host.displayValueWidth + + unitGap + + unitW + + extraGap + + host.displayExtraWidth + + Memory.RIGHT_PAD * drawScale; + host.setHudSize(boxW, boxH); + + float baseX = host.hudX(); + float baseY = host.hudY(); + float iconX = baseX + Memory.ICON_X * drawScale; + float iconY = baseY + (boxH - iconSize) * 0.5f; + float dividerX = baseX + Memory.DIVIDER_X * drawScale; + float dividerY = baseY + Memory.DIVIDER_Y * drawScale; + float dividerW = Math.max(0.5f, Memory.DIVIDER_WIDTH * drawScale); + float dividerH = Math.max(0.5f, boxH - 2f * Memory.DIVIDER_Y * drawScale); + float textY = baseY + (boxH - Math.max(valueH, unitH)) * 0.5f; + float unitX = valueX + host.displayValueWidth + unitGap; + float extraX = unitX + unitW + extraGap; + + int resolvedIconColor = host.uiIconColor; + boolean useLabelEffect = host.labelEffect.get() != HudTextEffects.Effect.NONE; + float time = useLabelEffect ? (float) (Util.getMillis() / 1000.0) : 0.0f; + if (useLabelEffect) { + resolvedIconColor = HudTextEffects.animatedColor(resolvedIconColor, host.labelEffect.get(), + host.labelEffectSpeed.get(), time, 0.0f); + } + + float radius = Memory.BOX_RADIUS * drawScale; + host.scriptModel.setVisible(true); + host.scriptModel.setRoot(baseX, baseY, boxW, boxH, radius, drawScale); + host.scriptModel.background().set(host.bgEffect.get(), host.isThemeMode(), host.blurAlpha.get() / 255.0f, + host.uiBgPrimary, host.uiBgSecondary, host.uiStroke, Math.max(0.5f, Memory.STROKE_WIDTH * drawScale), Memory.BOX_SOFTNESS); + host.scriptModel.icon().texture(Memory.MEMORY_ICON.toString(), iconX - baseX, iconY - baseY, iconSize, iconSize, resolvedIconColor); + host.scriptModel.divider().set(dividerX - baseX, dividerY - baseY, dividerW, dividerH, + HudRenderUtil.setAlpha(host.uiMetaColor, 0x52)); + host.scriptModel.value().set(host.cachedValue, "Onest", drawScale, valueX - baseX, textY - baseY, host.displayValueWidth, host.uiValueColor); + host.scriptModel.unit().set("MB", "OnestMedium", drawScale, unitX - baseX, textY - baseY, unitW, host.uiMetaColor); + host.scriptModel.extra().set(host.cachedExtra, "OnestMedium", drawScale, extraX - baseX, textY - baseY, host.displayExtraWidth, host.uiExtraColor); + host.scriptModel.animation().setLabelEffect(host.labelEffect.get().name(), host.labelEffectSpeed.get(), time) + .setDigitAnimation(false, "", 1.0f, 0.0f); + host.scriptModel.data("usedMiB", usedMiB) + .data("maxMiB", maxMiB) + .data("percent", percent) + .data("valueText", host.cachedValue) + .data("unitText", "MB") + .data("extraText", host.cachedExtra); + ScriptedCompactHudStatRenderer.INSTANCE.render(host.scriptModel, renderer, fallback, ctx, tickDelta); + } + + static float smoothWidth(float current, float target) { + if (current < 0.0f) return target; + if (target >= current) return target; + float next = AnimationUtility.approach(current, target, AnimationUtility.deltaTime(), 12.0f); + return AnimationUtility.snap(next, target, 0.25f); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/ping/PingFormat.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/ping/PingFormat.java new file mode 100644 index 0000000..d57492c --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/ping/PingFormat.java @@ -0,0 +1,21 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.stats; + +final class PingFormat { + + private PingFormat() { + } + + static String getPingValue(Ping host, int ping) { + if (ping == host.lastPing) return host.cachedValue; + host.lastPing = ping; + host.cachedValue = ping >= 0 ? Integer.toString(ping) : "--"; + return host.cachedValue; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/ping/PingPalette.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/ping/PingPalette.java new file mode 100644 index 0000000..a49236f --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/ping/PingPalette.java @@ -0,0 +1,44 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.stats; + +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; + +import static dev.loki.lovisual.features.theme.Theme.theme; + +final class PingPalette { + + private PingPalette() { + } + + static void update(Ping host) { + if (host.isThemeMode()) { + int panelAlpha = host.bgAlpha.get(); + host.uiBgPrimary = HudRenderUtil.setAlpha(theme().windowBg(), panelAlpha); + host.uiBgSecondary = HudRenderUtil.setAlpha( + HudRenderUtil.mixColor(theme().surface(), theme().windowHeader(), 0.35f), + panelAlpha + ); + host.uiStroke = HudRenderUtil.setAlpha( + HudRenderUtil.mixColor(theme().windowStroke(), theme().strokeSoft(), 0.4f), + Math.min(panelAlpha, 190) + ); + host.uiIconColor = theme().textPrimary(); + host.uiValueColor = theme().textPrimary(); + host.uiMetaColor = theme().textMuted(); + return; + } + + host.uiBgPrimary = host.bg.getArgb(); + host.uiBgSecondary = host.bg2.getArgb(); + host.uiStroke = host.stroke.getArgb(); + host.uiIconColor = host.iconColor.getArgb(); + host.uiValueColor = host.valueColor.getArgb(); + host.uiMetaColor = host.metaColor.getArgb(); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/ping/PingRender.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/ping/PingRender.java new file mode 100644 index 0000000..cd0cfdf --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/ping/PingRender.java @@ -0,0 +1,125 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.stats; + +import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; +import dev.loki.lovisual.features.gui.hud.script.ScriptedCompactHudStatRenderer; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.math.HudScale; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.font.Fonts; +import dev.loki.lovisual.render.engine.text.font.info.FontInfo; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.util.player.interaction.NetworkStatsUtil; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.util.Util; + +final class PingRender { + + private PingRender() { + } + + static void render(Ping host, + Renderer2D renderer, + TextRenderer textRenderer, + GuiGraphicsExtractor ctx, + float tickDelta, + int screenW, + int screenH) { + boolean preview = DraggableHudElementRegistry.isForceVisible(); + if (Minecraft.getInstance() == null && !preview) { + host.setHudSize(0f, 0f); + host.scriptModel.setVisible(false); + return; + } + if (!preview && !host.isEnabled()) { + host.setHudSize(0f, 0f); + host.scriptModel.setVisible(false); + return; + } + + int ping = NetworkStatsUtil.getPing(Minecraft.getInstance()); + String value = PingFormat.getPingValue(host, ping); + String unit = "ms"; + PingPalette.update(host); + + TextRenderer fallback = textRenderer != null ? textRenderer : TextRenderer.get(); + TextRenderer valueRenderer = Fonts.renderer("Onest", FontInfo.Type.Regular, fallback); + TextRenderer metaRenderer = Fonts.renderer("OnestMedium", FontInfo.Type.Regular, valueRenderer); + + float drawScale = HudScale.scale(screenW, screenH) + * (host.hud.getFontSize() / 18f) + * Ping.SCALE_MULT + * host.scale.get().floatValue(); + + valueRenderer.begin(drawScale, true, false); + float valueW = (float) valueRenderer.getWidth(value, false); + float valueH = (float) valueRenderer.getHeight(false); + valueRenderer.end(); + + metaRenderer.begin(drawScale, true, false); + float unitW = (float) metaRenderer.getWidth(unit, false); + float unitH = (float) metaRenderer.getHeight(false); + metaRenderer.end(); + + float boxH = Ping.BOX_HEIGHT * drawScale; + float iconSize = Ping.ICON_SIZE * drawScale; + float valueX = host.hudX() + Ping.TEXT_LEFT_X * drawScale; + float unitGap = Ping.VALUE_UNIT_GAP * drawScale; + host.displayValueWidth = smoothWidth(host.displayValueWidth, valueW); + float boxW = (valueX - host.hudX()) + host.displayValueWidth + unitGap + unitW + Ping.RIGHT_PAD * drawScale; + host.setHudSize(boxW, boxH); + + float baseX = host.hudX(); + float baseY = host.hudY(); + float iconX = baseX + Ping.ICON_X * drawScale; + float iconY = baseY + (boxH - iconSize) * 0.5f; + float dividerX = baseX + Ping.DIVIDER_X * drawScale; + float dividerY = baseY + Ping.DIVIDER_Y * drawScale; + float dividerW = Math.max(0.5f, Ping.DIVIDER_WIDTH * drawScale); + float dividerH = Math.max(0.5f, boxH - 2f * Ping.DIVIDER_Y * drawScale); + float textY = baseY + (boxH - Math.max(valueH, unitH)) * 0.5f; + float unitX = valueX + host.displayValueWidth + unitGap; + + int resolvedIconColor = host.uiIconColor; + boolean useLabelEffect = host.labelEffect.get() != HudTextEffects.Effect.NONE; + float time = useLabelEffect ? (float) (Util.getMillis() / 1000.0) : 0.0f; + if (useLabelEffect) { + resolvedIconColor = HudTextEffects.animatedColor(resolvedIconColor, host.labelEffect.get(), + host.labelEffectSpeed.get(), time, 0.0f); + } + + float radius = Ping.BOX_RADIUS * drawScale; + host.scriptModel.setVisible(true); + host.scriptModel.setRoot(baseX, baseY, boxW, boxH, radius, drawScale); + host.scriptModel.background().set(host.bgEffect.get(), host.isThemeMode(), host.blurAlpha.get() / 255.0f, + host.uiBgPrimary, host.uiBgSecondary, host.uiStroke, Math.max(0.5f, Ping.STROKE_WIDTH * drawScale), Ping.BOX_SOFTNESS); + host.scriptModel.icon().texture(Ping.PING_ICON.toString(), iconX - baseX, iconY - baseY, iconSize, iconSize, resolvedIconColor); + host.scriptModel.divider().set(dividerX - baseX, dividerY - baseY, dividerW, dividerH, + HudRenderUtil.setAlpha(host.uiMetaColor, 0x52)); + host.scriptModel.value().set(value, "Onest", drawScale, valueX - baseX, textY - baseY, host.displayValueWidth, host.uiValueColor); + host.scriptModel.unit().set(unit, "OnestMedium", drawScale, unitX - baseX, textY - baseY, unitW, host.uiMetaColor); + host.scriptModel.extra().hidden(); + host.scriptModel.animation().setLabelEffect(host.labelEffect.get().name(), host.labelEffectSpeed.get(), time) + .setDigitAnimation(false, "", 1.0f, 0.0f); + host.scriptModel.data("ping", ping) + .data("valueText", value) + .data("unitText", unit); + ScriptedCompactHudStatRenderer.INSTANCE.render(host.scriptModel, renderer, fallback, ctx, tickDelta); + } + + static float smoothWidth(float current, float target) { + if (current < 0.0f) return target; + if (target >= current) return target; + float next = AnimationUtility.approach(current, target, AnimationUtility.deltaTime(), 12.0f); + return AnimationUtility.snap(next, target, 0.25f); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/tps/TpsFormat.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/tps/TpsFormat.java new file mode 100644 index 0000000..b7aff72 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/tps/TpsFormat.java @@ -0,0 +1,65 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.stats; + +final class TpsFormat { + + private TpsFormat() { + } + + static void updateTpsStrings(Tps host, float tps, float extraTps, boolean extraEnabled) { + int tenth = Math.round(tps * 10f); + int hundredth = Math.round(extraTps * 100f); + if (extraEnabled == host.lastExtra + && tenth == host.lastTpsTenth + && (!extraEnabled || hundredth == host.lastExtraHundredth)) { + return; + } + + host.lastExtra = extraEnabled; + host.lastTpsTenth = tenth; + host.lastExtraHundredth = hundredth; + + host.tpsBuilder.setLength(0); + appendOneDecimal(host.tpsBuilder, tenth); + host.cachedValue = host.tpsBuilder.toString(); + + if (extraEnabled) { + host.extraBuilder.setLength(0); + host.extraBuilder.append('['); + appendTwoDecimals(host.extraBuilder, hundredth); + host.extraBuilder.append(']'); + host.cachedExtra = host.extraBuilder.toString(); + } else { + host.cachedExtra = ""; + } + } + + private static void appendOneDecimal(StringBuilder sb, int tenth) { + if (tenth < 0) { + sb.append('-'); + tenth = -tenth; + } + int intPart = tenth / 10; + int frac = tenth - intPart * 10; + sb.append(intPart).append('.').append((char) ('0' + frac)); + } + + private static void appendTwoDecimals(StringBuilder sb, int hundredth) { + if (hundredth < 0) { + sb.append('-'); + hundredth = -hundredth; + } + int intPart = hundredth / 100; + int frac = hundredth - intPart * 100; + sb.append(intPart) + .append('.') + .append((char) ('0' + (frac / 10))) + .append((char) ('0' + (frac % 10))); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/tps/TpsPalette.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/tps/TpsPalette.java new file mode 100644 index 0000000..f443b34 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/tps/TpsPalette.java @@ -0,0 +1,46 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.stats; + +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; + +import static dev.loki.lovisual.features.theme.Theme.theme; + +final class TpsPalette { + + private TpsPalette() { + } + + static void update(Tps host) { + if (host.isThemeMode()) { + int panelAlpha = host.bgAlpha.get(); + host.uiBgPrimary = HudRenderUtil.setAlpha(theme().windowBg(), panelAlpha); + host.uiBgSecondary = HudRenderUtil.setAlpha( + HudRenderUtil.mixColor(theme().surface(), theme().windowHeader(), 0.35f), + panelAlpha + ); + host.uiStroke = HudRenderUtil.setAlpha( + HudRenderUtil.mixColor(theme().windowStroke(), theme().strokeSoft(), 0.4f), + Math.min(panelAlpha, 190) + ); + host.uiIconColor = theme().textPrimary(); + host.uiValueColor = theme().textPrimary(); + host.uiMetaColor = theme().textMuted(); + host.uiExtraColor = HudRenderUtil.mixColor(theme().textMuted(), theme().textPrimary(), 0.18f); + return; + } + + host.uiBgPrimary = host.bg.getArgb(); + host.uiBgSecondary = host.bg2.getArgb(); + host.uiStroke = host.stroke.getArgb(); + host.uiIconColor = host.iconColor.getArgb(); + host.uiValueColor = host.valueColor.getArgb(); + host.uiMetaColor = host.metaColor.getArgb(); + host.uiExtraColor = host.extraColor.getArgb(); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/tps/TpsRender.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/tps/TpsRender.java new file mode 100644 index 0000000..0f4da92 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/stats/tps/TpsRender.java @@ -0,0 +1,143 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.stats; + +import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; +import dev.loki.lovisual.features.gui.hud.script.ScriptedCompactHudStatRenderer; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.math.HudScale; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.font.Fonts; +import dev.loki.lovisual.render.engine.text.font.info.FontInfo; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.util.player.interaction.NetworkStatsUtil; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.util.Util; + +final class TpsRender { + + private TpsRender() { + } + + static void render(Tps host, + Renderer2D renderer, + TextRenderer textRenderer, + GuiGraphicsExtractor ctx, + float tickDelta, + int screenW, + int screenH) { + boolean preview = DraggableHudElementRegistry.isForceVisible(); + if (Minecraft.getInstance() == null && !preview) { + host.setHudSize(0f, 0f); + host.scriptModel.setVisible(false); + return; + } + if (!preview && !host.isEnabled()) { + host.setHudSize(0f, 0f); + host.scriptModel.setVisible(false); + return; + } + + Minecraft mc = Minecraft.getInstance(); + float tps = NetworkStatsUtil.getTps(mc); + boolean showExtra = host.extra.get(); + float clientTps = showExtra ? NetworkStatsUtil.getClientTps(mc) : 0f; + TpsFormat.updateTpsStrings(host, tps, clientTps, showExtra); + TpsPalette.update(host); + + TextRenderer fallback = textRenderer != null ? textRenderer : TextRenderer.get(); + TextRenderer valueRenderer = Fonts.renderer("Onest", FontInfo.Type.Regular, fallback); + TextRenderer metaRenderer = Fonts.renderer("OnestMedium", FontInfo.Type.Regular, valueRenderer); + + float drawScale = HudScale.scale(screenW, screenH) + * (host.hud.getFontSize() / 18f) + * Tps.SCALE_MULT + * host.scale.get().floatValue(); + + valueRenderer.begin(drawScale, true, false); + float valueW = (float) valueRenderer.getWidth(host.cachedValue, false); + float valueH = (float) valueRenderer.getHeight(false); + valueRenderer.end(); + + metaRenderer.begin(drawScale, true, false); + float unitW = (float) metaRenderer.getWidth("TPS", false); + float unitH = (float) metaRenderer.getHeight(false); + float extraW = showExtra ? (float) metaRenderer.getWidth(host.cachedExtra, false) : 0f; + metaRenderer.end(); + + float boxH = Tps.BOX_HEIGHT * drawScale; + float iconSize = Tps.ICON_SIZE * drawScale; + float valueX = host.hudX() + Tps.TEXT_LEFT_X * drawScale; + float unitGap = Tps.VALUE_UNIT_GAP * drawScale; + float extraGap = Tps.EXTRA_GAP * drawScale; + host.displayValueWidth = smoothWidth(host.displayValueWidth, valueW); + host.displayExtraWidth = showExtra ? smoothWidth(host.displayExtraWidth, extraW) : 0.0f; + float boxW = (valueX - host.hudX()) + + host.displayValueWidth + + unitGap + + unitW + + (showExtra ? extraGap + host.displayExtraWidth : 0f) + + Tps.RIGHT_PAD * drawScale; + host.setHudSize(boxW, boxH); + + float baseX = host.hudX(); + float baseY = host.hudY(); + float iconX = baseX + Tps.ICON_X * drawScale; + float iconY = baseY + (boxH - iconSize) * 0.5f; + float dividerX = baseX + Tps.DIVIDER_X * drawScale; + float dividerY = baseY + Tps.DIVIDER_Y * drawScale; + float dividerW = Math.max(0.5f, Tps.DIVIDER_WIDTH * drawScale); + float dividerH = Math.max(0.5f, boxH - 2f * Tps.DIVIDER_Y * drawScale); + float textY = baseY + (boxH - Math.max(valueH, unitH)) * 0.5f; + float unitX = valueX + host.displayValueWidth + unitGap; + float extraX = unitX + unitW + extraGap; + + int resolvedIconColor = host.uiIconColor; + boolean useLabelEffect = host.labelEffect.get() != HudTextEffects.Effect.NONE; + float time = useLabelEffect ? (float) (Util.getMillis() / 1000.0) : 0.0f; + if (useLabelEffect) { + resolvedIconColor = HudTextEffects.animatedColor(resolvedIconColor, host.labelEffect.get(), + host.labelEffectSpeed.get(), time, 0.0f); + } + + float radius = Tps.BOX_RADIUS * drawScale; + host.scriptModel.setVisible(true); + host.scriptModel.setRoot(baseX, baseY, boxW, boxH, radius, drawScale); + host.scriptModel.background().set(host.bgEffect.get(), host.isThemeMode(), host.blurAlpha.get() / 255.0f, + host.uiBgPrimary, host.uiBgSecondary, host.uiStroke, Math.max(0.5f, Tps.STROKE_WIDTH * drawScale), Tps.BOX_SOFTNESS); + host.scriptModel.icon().texture(Tps.TPS_ICON.toString(), iconX - baseX, iconY - baseY, iconSize, iconSize, resolvedIconColor); + host.scriptModel.divider().set(dividerX - baseX, dividerY - baseY, dividerW, dividerH, + HudRenderUtil.setAlpha(host.uiMetaColor, 0x52)); + host.scriptModel.value().set(host.cachedValue, "Onest", drawScale, valueX - baseX, textY - baseY, host.displayValueWidth, host.uiValueColor); + host.scriptModel.unit().set("TPS", "OnestMedium", drawScale, unitX - baseX, textY - baseY, unitW, host.uiMetaColor); + if (showExtra) { + host.scriptModel.extra().set(host.cachedExtra, "OnestMedium", drawScale, extraX - baseX, textY - baseY, host.displayExtraWidth, host.uiExtraColor); + } else { + host.scriptModel.extra().hidden(); + } + host.scriptModel.animation().setLabelEffect(host.labelEffect.get().name(), host.labelEffectSpeed.get(), time) + .setDigitAnimation(false, "", 1.0f, 0.0f); + host.scriptModel.data("tps", tps) + .data("clientTps", clientTps) + .data("showExtra", showExtra) + .data("valueText", host.cachedValue) + .data("unitText", "TPS") + .data("extraText", host.cachedExtra); + ScriptedCompactHudStatRenderer.INSTANCE.render(host.scriptModel, renderer, fallback, ctx, tickDelta); + } + + private static float smoothWidth(float current, float target) { + if (current < 0.0f) return target; + if (target >= current) return target; + float next = AnimationUtility.approach(current, target, AnimationUtility.deltaTime(), 12.0f); + return AnimationUtility.snap(next, target, 0.25f); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Admins.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Admins.java new file mode 100644 index 0000000..a41085b --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Admins.java @@ -0,0 +1,118 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.status; + +import dev.loki.lovisual.config.definition.SettingDef; +import dev.loki.lovisual.config.values.color.RGBAColorValue; +import dev.loki.lovisual.config.values.color.RGBColorValue; +import dev.loki.lovisual.config.values.mode.ModeValue; +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; +import dev.loki.lovisual.features.gui.hud.draggable.impl.HudPanelLayoutModes; +import dev.loki.lovisual.features.gui.hud.draggable.impl.scripted.ScriptedListHudPanel; +import dev.loki.lovisual.features.gui.hud.registry.HudElementRegister; +import dev.loki.lovisual.features.relations.StaffData; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import net.minecraft.client.gui.GuiGraphicsExtractor; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@HudElementRegister(order = 50) +public final class Admins extends DraggableHudElement { + + { + defaultLinkedLayout(16.0f, 212.0f, "keybinds", "RIGHT", 0.0f, 0.0f); + } + + static final float HEADER_HEIGHT = 15.5f; + static final float CONTENT_START_Y = 25.0f; + static final float ROW_STEP = 11.0f; + static final float MIN_WIDTH = 96.0f; + static final float ROW_DIVIDER_X = 15.0f; + static final float ROW_DIVIDER_H = 6.0f; + static final float TITLE_TEXT_X = 22.0f; + static final float COUNT_LABEL_OFFSET = 22.0f; + static final float ROW_HEAD_X = 3.5f; + static final float ROW_HEAD_SIZE = 8.0f; + static final float ROW_TEXT_X = 18.0f; + static final float ROW_STATUS_RIGHT = 8.0f; + static final float ROW_CONTENT_CENTER_OFFSET = 1.95f; + static final String COLOR_THEME = "Theme"; + static final String COLOR_CUSTOM = "Custom"; + static final String EFFECT_NONE = "None"; + static final String EFFECT_BLUR = "Blur"; + + final HudGlobalConfig hud = HudGlobalConfig.get(); + final ScriptedListHudPanel scriptedPanel = new ScriptedListHudPanel(); + final NumberValue scaleValue = new NumberValue<>("admins_scale", 1.0, HudPanelLayoutModes.SCALE_MIN, HudPanelLayoutModes.SCALE_MAX); + final ModeValue layoutMode = new ModeValue("admins_layout", "Split Header", HudPanelLayoutModes.SPLIT_HEADER, HudPanelLayoutModes.UNIFIED_DIVIDER); + final ModeValue colorMode = new ModeValue("admins_color_mode", "Theme", COLOR_THEME, COLOR_CUSTOM); + final ModeValue bgEffect = new ModeValue("admins_bg_effect", "Blur", EFFECT_NONE, EFFECT_BLUR); + final RGBAColorValue bg = new RGBAColorValue("admins_bg", "#EB111318"); + final RGBAColorValue bg2 = new RGBAColorValue("admins_bg_secondary", "#F7161616"); + final NumberValue bgAlpha = new NumberValue<>("admins_bg_alpha", 131, 0, 255); + final RGBColorValue stroke = new RGBColorValue("admins_stroke", "#5A5A5A"); + final RGBColorValue text = new RGBColorValue("admins_text", "#FFFFFF"); + final RGBColorValue muted = new RGBColorValue("admins_muted", "#A5A5A5"); + final NumberValue blurAlpha = new NumberValue<>("admins_blur_alpha", 255, 0, 255); + final BooleanValue headerIconPulse = new BooleanValue("admins_header_icon_pulse", true); + final NumberValue headerIconPulseSpeed = new NumberValue<>("admins_header_icon_pulse_speed", 18, 1, 60); + final NumberValue headerIconPulseIntensity = new NumberValue<>("admins_header_icon_pulse_intensity", 100, 0, 100); + + final Map entryAnim = new LinkedHashMap<>(); + final Map lastEntries = new LinkedHashMap<>(); + float displayWidth = -1.0f; + float displayHeight = -1.0f; + float visibilityAnim = 0.0f; + int uiHeaderLeft; int uiHeaderRight; int uiBodyLeft; int uiBodyRight; int uiOutline; int uiText; int uiMuted; int uiCounter; int uiTitleText; int uiDivider; int uiBlurTint; + + public Admins() { super("admins", "Admins", true); } + + @Override + protected void defineSettings(List defs) { + defs.add(SettingDef.number(scaleValue)); + defs.add(SettingDef.mode(layoutMode)); + defs.add(SettingDef.mode(colorMode)); + defs.add(SettingDef.color(bg).visibleWhen(this::isCustomMode)); + defs.add(SettingDef.color(bg2).visibleWhen(this::isCustomMode)); + defs.add(SettingDef.colorNoAlpha(stroke).visibleWhen(this::isCustomMode)); + defs.add(SettingDef.number(bgAlpha).visibleWhen(this::isThemeMode)); + defs.add(SettingDef.colorNoAlpha(text).visibleWhen(this::isCustomMode)); + defs.add(SettingDef.colorNoAlpha(muted).visibleWhen(this::isCustomMode)); + defs.add(SettingDef.mode(bgEffect)); + defs.add(SettingDef.number(blurAlpha).visibleWhen(this::hasEffect)); + defs.add(SettingDef.bool(headerIconPulse)); + defs.add(SettingDef.number(headerIconPulseSpeed).visibleWhen(headerIconPulse::get)); + defs.add(SettingDef.number(headerIconPulseIntensity).visibleWhen(headerIconPulse::get)); + } + + @Override public void applyDefaultPosition(int screenW, int screenH) { this.x = screenW - 200.0f; this.y = 80.0f; } + @Override public boolean usesEngineRenderer() { return true; } + + @Override + public void renderEngine(Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, + float tickDelta, int screenW, int screenH) { + AdminsRenderer.render(this, renderer, textRenderer, ctx, tickDelta, screenW, screenH); + } + + @Override public boolean isDraggable() { return super.isDraggable(); } + + boolean isThemeMode() { return AdminsPalette.isThemeMode(this); } + boolean isCustomMode() { return AdminsPalette.isCustomMode(this); } + boolean hasEffect() { return AdminsPalette.hasEffect(this); } + + void setSize(float w, float h) { setBounds(getX(), getY(), w, h); } + void setEmpty() { setBounds(getX(), getY(), 0.0f, 0.0f); } + + record AnimatedEntry(StaffData.StaffEntry entry, float anim) {} +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Armor.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Armor.java new file mode 100644 index 0000000..6495943 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Armor.java @@ -0,0 +1,162 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.status; + +import dev.loki.lovisual.config.values.mode.EnumValue; +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import dev.loki.lovisual.features.gui.hud.registry.HudElementInfo; +import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; +import dev.loki.lovisual.render.engine.math.HudScale; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; + +//todo Description +@HudElementInfo( + id = "armor", + displayName = "Armor", + enabledByDefault = false, + order = 210 +) +public final class Armor extends DraggableHudElement { + + { + defaultLayout(1616.315f, 1003.6977f); + } + + static final int SLOTS = 4; + static final float DEFAULT_SLOT_SIZE = 18f; + static final float DEFAULT_SLOT_GAP = 2f; + static final float PANEL_RADIUS = 4.0f; + static final float GLASS_BLUR_STRENGTH = 1.0f; + static final float GLASS_ALPHA = 1.0f; + static final float DEFAULT_MARGIN = 16f; + static final float DIVIDER_THICKNESS = 0.5f; + + final HudGlobalConfig hud = HudGlobalConfig.get(); + final NumberValue scaleValue = num("armor_scale", 3.68, 0.5, 5.0); + final EnumValue layoutMode = + enumSetting("armor_layout", LayoutMode.HORIZONTAL, LayoutMode.VERTICAL, LayoutMode.HORIZONTAL); + final BooleanValue reverse = bool("armor_reverse", false); + final EnumValue stateMode = + enumSetting("armor_state_mode", StateMode.TEXT, StateMode.BAR, StateMode.TEXT); + final NumberValue stateBarThreshold = + visibleWhen(num("armor_state_bar_threshold", 70, 0, 100), () -> stateMode.get() == StateMode.BAR); + final NumberValue stateThreshold = + visibleWhen(num("armor_state_threshold", 70, 0, 100), () -> stateMode.get() == StateMode.TEXT); + final NumberValue stateColorThreshold = + visibleWhen(num("armor_state_color_threshold", 70, 0, 100), () -> stateMode.get() == StateMode.TEXT); + float baseX; + float baseY; + float slotSize; + float slotGap; + float slotRadius; + float itemScale; + boolean layoutReady; + net.minecraft.world.item.ItemStack[] cachedStacks; + float renderX; + float renderY; + + @Override + public void applyDefaultPosition(int screenW, int screenH) { + float scale = HudScale.scale(screenW, screenH) * scaleValue.get().floatValue(); + float size = DEFAULT_SLOT_SIZE * scale; + float gap = DEFAULT_SLOT_GAP * scale; + boolean horizontal = isHorizontal(); + float w = horizontal ? SLOTS * size + (SLOTS - 1) * gap : size; + float h = horizontal ? size : SLOTS * size + (SLOTS - 1) * gap; + if (horizontal) { + this.x = screenW * 0.5f - w * 0.5f; + this.y = screenH - h - DEFAULT_MARGIN * scale; + } else { + this.x = screenW - w - DEFAULT_MARGIN * scale; + this.y = screenH * 0.5f - h * 0.5f; + } + } + + @Override + public boolean usesEngineRenderer() { + return true; + } + + @Override + public void renderEngine(Renderer2D renderer, + TextRenderer textRenderer, + GuiGraphicsExtractor ctx, + float tickDelta, + int screenW, + int screenH) { + ArmorRenderer.render(this, renderer, textRenderer, ctx, tickDelta, screenW, screenH); + } + + @Override + public void renderEngineForeground(Renderer2D renderer, + TextRenderer textRenderer, + GuiGraphicsExtractor ctx, + float tickDelta, + int screenW, + int screenH) { + ArmorRenderer.renderForeground(this, renderer, textRenderer, ctx, tickDelta, screenW, screenH); + } + + boolean isHorizontal() { + return layoutMode.get() == LayoutMode.HORIZONTAL; + } + + boolean isReverseOrder() { + return reverse.get(); + } + + void setArmorSize(float w, float h) { + setBounds(getX(), getY(), w, h); + } + + void setEmptySize() { + setBounds(getX(), getY(), 0f, 0f); + } + + @Override + public boolean contains(float mx, float my) { + return mx >= renderX && mx <= renderX + width && my >= renderY && my <= renderY + height; + } + + enum LayoutMode implements EnumValue.IdProvider { + VERTICAL("Vertical"), + HORIZONTAL("Horizontal"); + + private final String id; + + LayoutMode(String id) { + this.id = id; + } + + @Override + public String id() { + return id; + } + } + + enum StateMode implements EnumValue.IdProvider { + BAR("Bar"), + TEXT("Text"); + + private final String id; + + StateMode(String id) { + this.id = id; + } + + @Override + public String id() { + return id; + } + } + +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Cooldowns.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Cooldowns.java new file mode 100644 index 0000000..6b9e1b7 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Cooldowns.java @@ -0,0 +1,151 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.status; + +import dev.loki.lovisual.config.definition.SettingDef; +import dev.loki.lovisual.config.values.color.RGBAColorValue; +import dev.loki.lovisual.config.values.color.RGBColorValue; +import dev.loki.lovisual.config.values.mode.ModeValue; +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; +import dev.loki.lovisual.features.gui.hud.draggable.impl.HudPanelLayoutModes; +import dev.loki.lovisual.features.gui.hud.draggable.impl.scripted.ScriptedListHudPanel; +import dev.loki.lovisual.features.gui.hud.registry.HudElementRegister; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@HudElementRegister(order = 30) +public final class Cooldowns extends DraggableHudElement { + + { + defaultLayout(1160.4823f, 466.0f); + } + + static final float HEADER_HEIGHT = 15.5f; + static final float BODY_Y_OFFSET = 18.5f; + static final float CONTENT_START_Y = 25.0f; + static final float BODY_INSET_Y = 6.5f; + static final float ROW_STEP = 11.0f; + static final float MIN_WIDTH = 110.0f; + static final float PANEL_RADIUS = 4.0f; + static final float PANEL_SOFTNESS = 1.0f; + static final float PANEL_STROKE = 0.55f; + static final float HEADER_DIVIDER_X = 18.0f; + static final float HEADER_DIVIDER_Y = 5.0f; + static final float HEADER_DIVIDER_H = 6.0f; + static final float ROW_DIVIDER_X = 15.0f; + static final float ROW_DIVIDER_H = 6.0f; + static final float TITLE_ICON_X = 4.0f; + static final float TITLE_TEXT_X = 22.0f; + static final float COUNT_LABEL_OFFSET = 22.0f; + static final float COUNT_VALUE_OFFSET = 2.5f; + static final float ROW_ICON_X = 3.5f; + static final float ROW_ICON_VISUAL_SIZE = 8.0f; + static final float ROW_TEXT_X = 18.0f; + static final float ROW_TIME_RIGHT = 8.0f; + static final float ROW_CONTENT_CENTER_OFFSET = 1.9f; + static final int BLINK_THRESHOLD_TICKS = 100; + static final double BLINK_SPEED = 0.008; + static final String COLOR_THEME = "Theme"; + static final String COLOR_CUSTOM = "Custom"; + static final String EFFECT_NONE = "None"; + static final String EFFECT_BLUR = "Blur"; + static final int PVP_ACTIVE_COLOR = 0xFFFFA500; + static final int PVP_IDLE_COLOR = 0xFF00A0FF; + static final int PVP_BLOCKED_COLOR = 0xFFFF5656; + static final Item[] PREVIEW_ITEMS = new Item[]{Items.ENDER_EYE, Items.ENDER_PEARL, Items.SUGAR, Items.MACE, Items.ENCHANTED_GOLDEN_APPLE, Items.TRIDENT, Items.CROSSBOW, Items.DRIED_KELP, Items.NETHERITE_SCRAP}; + static ItemStack[] previewStacks; + + final HudGlobalConfig hud = HudGlobalConfig.get(); + final ScriptedListHudPanel scriptedPanel = new ScriptedListHudPanel(); + final NumberValue scaleValue = new NumberValue<>("cooldowns_scale", 1.0, HudPanelLayoutModes.SCALE_MIN, HudPanelLayoutModes.SCALE_MAX); + final ModeValue layoutMode = new ModeValue("cooldowns_layout", "Split Header", HudPanelLayoutModes.SPLIT_HEADER, HudPanelLayoutModes.UNIFIED_DIVIDER); + final ModeValue colorMode = new ModeValue("cooldowns_color_mode", "Theme", COLOR_THEME, COLOR_CUSTOM); + final ModeValue bgEffect = new ModeValue("cooldowns_bg_effect", "Blur", EFFECT_NONE, EFFECT_BLUR); + final BooleanValue blink = new BooleanValue("cooldowns_blink", true); + final RGBAColorValue bg = new RGBAColorValue("cooldowns_bg", "#EB111318"); + final RGBAColorValue bg2 = new RGBAColorValue("cooldowns_bg_secondary", "#F7161616"); + final NumberValue bgAlpha = new NumberValue<>("cooldowns_bg_alpha", 185, 0, 255); + final RGBColorValue stroke = new RGBColorValue("cooldowns_stroke", "#5A5A5A"); + final RGBColorValue text = new RGBColorValue("cooldowns_text", "#FFFFFF"); + final RGBColorValue muted = new RGBColorValue("cooldowns_muted", "#A5A5A5"); + final NumberValue blurAlpha = new NumberValue<>("cooldowns_blur_alpha", 255, 0, 255); + final BooleanValue headerIconPulse = new BooleanValue("cooldowns_header_icon_pulse", true); + final NumberValue headerIconPulseSpeed = new NumberValue<>("cooldowns_header_icon_pulse_speed", 18, 1, 60); + final NumberValue headerIconPulseIntensity = new NumberValue<>("cooldowns_header_icon_pulse_intensity", 100, 0, 100); + + final Map entryAnim = new LinkedHashMap<>(); + final Map lastEntries = new LinkedHashMap<>(); + final List pendingItemTasks = new ArrayList<>(9); + float displayWidth = -1.0f; + float displayHeight = -1.0f; + float visibilityAnim = 0.0f; + boolean foregroundReady; + int uiHeaderLeft; int uiHeaderRight; int uiBodyLeft; int uiBodyRight; int uiOutline; int uiText; int uiMuted; int uiCounter; int uiTitleText; int uiDivider; int uiBlurTint; + + public Cooldowns() { super("cooldowns", "Cooldowns", true); } + + @Override + protected void defineSettings(List defs) { + defs.add(SettingDef.number(scaleValue)); + defs.add(SettingDef.mode(layoutMode)); + defs.add(SettingDef.mode(colorMode)); + defs.add(SettingDef.color(bg).visibleWhen(this::isCustomMode)); + defs.add(SettingDef.color(bg2).visibleWhen(this::isCustomMode)); + defs.add(SettingDef.colorNoAlpha(stroke).visibleWhen(this::isCustomMode)); + defs.add(SettingDef.number(bgAlpha).visibleWhen(this::isThemeMode)); + defs.add(SettingDef.colorNoAlpha(text).visibleWhen(this::isCustomMode)); + defs.add(SettingDef.colorNoAlpha(muted).visibleWhen(this::isCustomMode)); + defs.add(SettingDef.mode(bgEffect)); + defs.add(SettingDef.number(blurAlpha).visibleWhen(this::hasEffect)); + defs.add(SettingDef.bool(blink)); + defs.add(SettingDef.bool(headerIconPulse)); + defs.add(SettingDef.number(headerIconPulseSpeed).visibleWhen(headerIconPulse::get)); + defs.add(SettingDef.number(headerIconPulseIntensity).visibleWhen(headerIconPulse::get)); + } + + @Override public void applyDefaultPosition(int screenW, int screenH) { this.x = screenW - 132.0f; this.y = 20.0f; } + @Override public boolean usesEngineRenderer() { return true; } + + @Override + public void renderEngine(Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta, int screenW, int screenH) { + CooldownsRenderer.render(this, renderer, textRenderer, ctx, tickDelta, screenW, screenH); + } + + @Override + public void renderEngineForeground(Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta, int screenW, int screenH) { + if (!foregroundReady || ctx == null || pendingItemTasks.isEmpty()) { pendingItemTasks.clear(); return; } + for (QueuedItemIcon task : pendingItemTasks) renderer.item(task.stack(), task.x(), task.y(), task.scale(), task.seed(), Renderer2D.ITEM_OVERLAY_NONE, null); + pendingItemTasks.clear(); + } + + @Override public boolean isDraggable() { return super.isDraggable(); } + + boolean isThemeMode() { return CooldownsPalette.isThemeMode(this); } + boolean isCustomMode() { return CooldownsPalette.isCustomMode(this); } + boolean hasEffect() { return CooldownsPalette.hasEffect(this); } + boolean isForceVisibleForRenderer() { return dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry.isForceVisible() || dev.loki.lovisual.util.screen.ClientScreen.current() instanceof net.minecraft.client.gui.screens.ChatScreen; } + + void setSize(float w, float h) { setBounds(getX(), getY(), w, h); } + void setEmpty() { setBounds(getX(), getY(), 0.0f, 0.0f); } + + record Row(String key, ItemStack stack, String name, String duration, int remainingTicks, int timeColor) {} + record AnimatedRow(Row row, float anim) {} + record QueuedItemIcon(ItemStack stack, float x, float y, float scale, int seed) {} +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Potions.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Potions.java new file mode 100644 index 0000000..c225054 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/Potions.java @@ -0,0 +1,176 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.status; + +import dev.loki.lovisual.config.definition.SettingDef; +import dev.loki.lovisual.config.values.color.RGBAColorValue; +import dev.loki.lovisual.config.values.color.RGBColorValue; +import dev.loki.lovisual.config.values.mode.ModeValue; +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElement; +import dev.loki.lovisual.features.gui.hud.draggable.impl.HudPanelLayoutModes; +import dev.loki.lovisual.features.gui.hud.draggable.impl.scripted.ScriptedListHudPanel; +import dev.loki.lovisual.features.gui.hud.registry.HudElementRegister; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.resources.Identifier; +import net.minecraft.world.effect.MobEffectInstance; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@HudElementRegister(order = 20) +public final class Potions extends DraggableHudElement { + + { + defaultLayout(31.729004f, 182.0f); + } + + static final float HEADER_HEIGHT = 15.5f; + static final float BODY_Y_OFFSET = 18.5f; + static final float CONTENT_START_Y = 25.0f; + static final float BODY_INSET_Y = 6.5f; + static final float ROW_STEP = 11.0f; + static final float MIN_WIDTH = 95.0f; + static final float PANEL_RADIUS = 4.0f; + static final float PANEL_SOFTNESS = 1.0f; + static final float PANEL_STROKE = 0.55f; + static final float HEADER_DIVIDER_X = 18.0f; + static final float HEADER_DIVIDER_Y = 5.0f; + static final float HEADER_DIVIDER_H = 6.0f; + static final float ROW_DIVIDER_X = 14.0f; + static final float ROW_DIVIDER_H = 7.0f; + static final float TITLE_ICON_X = 5.0f; + static final float TITLE_TEXT_X = 22.0f; + static final float COUNT_LABEL_OFFSET = 22.0f; + static final float COUNT_VALUE_OFFSET = 3.0f; + static final float ROW_ICON_X = 3.5f; + static final float ROW_ICON_SIZE = 8.0f; + static final float ROW_TEXT_X = 18.0f; + static final float ROW_TIME_RIGHT = 8.0f; + static final float ROW_CONTENT_CENTER_OFFSET = 2.1f; + static final int BLINK_THRESHOLD_TICKS = 200; + static final double BLINK_SPEED = 0.009; + static final String COLOR_THEME = "Theme"; + static final String COLOR_CUSTOM = "Custom"; + static final String EFFECT_NONE = "None"; + static final String EFFECT_BLUR = "Blur"; + static final int NEGATIVE_EFFECT_RGB = 0xFFFF554B; + static final Identifier[] PREVIEW_ICONS = new Identifier[]{ + Identifier.fromNamespaceAndPath("minecraft", "textures/mob_effect/speed.png"), + Identifier.fromNamespaceAndPath("minecraft", "textures/mob_effect/haste.png"), + Identifier.fromNamespaceAndPath("minecraft", "textures/mob_effect/fire_resistance.png"), + Identifier.fromNamespaceAndPath("minecraft", "textures/mob_effect/regeneration.png"), + Identifier.fromNamespaceAndPath("minecraft", "textures/mob_effect/night_vision.png") + }; + + final HudGlobalConfig hud = HudGlobalConfig.get(); + final ScriptedListHudPanel scriptedPanel = new ScriptedListHudPanel(); + final NumberValue scaleValue = new NumberValue<>("potions_scale", 1.05, HudPanelLayoutModes.SCALE_MIN, HudPanelLayoutModes.SCALE_MAX); + final ModeValue layoutMode = new ModeValue("potions_layout", "Unified Divider", HudPanelLayoutModes.SPLIT_HEADER, HudPanelLayoutModes.UNIFIED_DIVIDER); + final ModeValue colorMode = new ModeValue("potions_color_mode", "Theme", COLOR_THEME, COLOR_CUSTOM); + final ModeValue bgEffect = new ModeValue("potions_bg_effect", "Blur", EFFECT_NONE, EFFECT_BLUR); + final RGBAColorValue bg = new RGBAColorValue("potions_bg", "#EB784F4F"); + final RGBAColorValue bg2 = new RGBAColorValue("potions_bg_secondary", "#F7161616"); + final NumberValue bgAlpha = new NumberValue<>("potions_bg_alpha", 200, 0, 255); + final RGBColorValue stroke = new RGBColorValue("potions_stroke", "#5A5A5A"); + final RGBColorValue text = new RGBColorValue("potions_text", "#FFFFFF"); + final RGBColorValue muted = new RGBColorValue("potions_muted", "#A5A5A5"); + final NumberValue blurAlpha = new NumberValue<>("potions_blur_alpha", 159, 0, 255); + final BooleanValue headerIconPulse = new BooleanValue("potions_header_icon_pulse", true); + final NumberValue headerIconPulseSpeed = new NumberValue<>("potions_header_icon_pulse_speed", 18, 1, 60); + final NumberValue headerIconPulseIntensity = new NumberValue<>("potions_header_icon_pulse_intensity", 100, 0, 100); + + final Map entryAnim = new LinkedHashMap<>(); + final Map lastEntries = new LinkedHashMap<>(); + float displayWidth = -1.0f; + float displayHeight = -1.0f; + float visibilityAnim = 0.0f; + int uiHeaderLeft; + int uiHeaderRight; + int uiBodyLeft; + int uiBodyRight; + int uiOutline; + int uiText; + int uiMuted; + int uiCounter; + int uiTitleText; + int uiAmpText; + int uiDivider; + int uiBlurTint; + + public Potions() { + super("potions", "Potions", true); + } + + @Override + protected void defineSettings(List defs) { + defs.add(SettingDef.number(scaleValue)); + defs.add(SettingDef.mode(layoutMode)); + defs.add(SettingDef.mode(colorMode)); + defs.add(SettingDef.color(bg).visibleWhen(this::isCustomMode)); + defs.add(SettingDef.color(bg2).visibleWhen(this::isCustomMode)); + defs.add(SettingDef.colorNoAlpha(stroke).visibleWhen(this::isCustomMode)); + defs.add(SettingDef.number(bgAlpha).visibleWhen(this::isThemeMode)); + defs.add(SettingDef.colorNoAlpha(text).visibleWhen(this::isCustomMode)); + defs.add(SettingDef.colorNoAlpha(muted).visibleWhen(this::isCustomMode)); + defs.add(SettingDef.mode(bgEffect)); + defs.add(SettingDef.number(blurAlpha).visibleWhen(this::hasEffect)); + defs.add(SettingDef.bool(headerIconPulse)); + defs.add(SettingDef.number(headerIconPulseSpeed).visibleWhen(headerIconPulse::get)); + defs.add(SettingDef.number(headerIconPulseIntensity).visibleWhen(headerIconPulse::get)); + } + + @Override + public void applyDefaultPosition(int screenW, int screenH) { + this.x = screenW - 120.0f; + this.y = 20.0f; + } + + @Override + public boolean usesEngineRenderer() { + return true; + } + + @Override + public void renderEngine(Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta, int screenW, int screenH) { + PotionsRenderer.render(this, renderer, textRenderer, ctx, tickDelta, screenW, screenH); + } + + @Override + public boolean isDraggable() { + return super.isDraggable(); + } + + boolean isThemeMode() { + return PotionsPalette.isThemeMode(this); + } + + boolean isCustomMode() { + return PotionsPalette.isCustomMode(this); + } + + boolean hasEffect() { + return PotionsPalette.hasEffect(this); + } + + void setSize(float w, float h) { + setBounds(getX(), getY(), w, h); + } + + void setEmpty() { + setBounds(getX(), getY(), 0.0f, 0.0f); + } + + record Row(String key, MobEffectInstance effect, String name, String amp, String duration, Identifier iconId) {} + record AnimatedRow(Row row, float anim) {} +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/admins/AdminsAnimator.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/admins/AdminsAnimator.java new file mode 100644 index 0000000..356efff --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/admins/AdminsAnimator.java @@ -0,0 +1,74 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.status; + +import dev.loki.lovisual.features.relations.StaffData; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +final class AdminsAnimator { + + private AdminsAnimator() {} + + static List animateEntries(Admins host, List entries) { + Map current = new LinkedHashMap<>(); + for (StaffData.StaffEntry entry : entries) { + current.put(entry.name(), entry); + } + Map merged = new LinkedHashMap<>(); + merged.putAll(current); + for (Map.Entry entry : host.lastEntries.entrySet()) { + merged.putIfAbsent(entry.getKey(), entry.getValue()); + } + host.lastEntries.clear(); + host.lastEntries.putAll(merged); + + List toRemove = new ArrayList<>(); + for (Map.Entry entry : host.lastEntries.entrySet()) { + String key = entry.getKey(); + float target = current.containsKey(key) ? 1.0f : 0.0f; + float anim = host.entryAnim.getOrDefault(key, 0.0f); + anim = AnimationUtility.approach(anim, target, AnimationUtility.deltaTime(), 12.0f); + anim = AnimationUtility.snap(anim, target, 0.02f); + if (anim <= 0.0f && target <= 0.0f) { + toRemove.add(key); + } else { + host.entryAnim.put(key, anim); + } + } + for (String key : toRemove) { + host.entryAnim.remove(key); + host.lastEntries.remove(key); + } + + List out = new ArrayList<>(); + for (Map.Entry entry : host.lastEntries.entrySet()) { + float anim = host.entryAnim.getOrDefault(entry.getKey(), 0.0f); + if (anim > 0.01f) { + out.add(new Admins.AnimatedEntry(entry.getValue(), anim)); + } + } + return out; + } + + static float totalAnimatedHeight(List rows, float rowStep) { + float out = 0.0f; + for (Admins.AnimatedEntry row : rows) { + out += rowStep * row.anim(); + } + return out; + } + + static int clamp255(int value) { + return Math.max(0, Math.min(255, value)); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/admins/AdminsData.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/admins/AdminsData.java new file mode 100644 index 0000000..63eb272 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/admins/AdminsData.java @@ -0,0 +1,138 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.status; + +import dev.loki.lovisual.features.relations.StaffData; +import dev.loki.lovisual.features.relations.StaffTracker; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.util.player.health.PlayerSkinResolver; +import dev.loki.lovisual.util.text.format.LegacyTextUtil; +import dev.loki.lovisual.util.text.format.TextRenderUtil; +import dev.loki.lovisual.util.text.format.TextRenderUtil.Part; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.PlayerInfo; +import net.minecraft.client.player.AbstractClientPlayer; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.player.Player; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; + +import static dev.loki.lovisual.features.theme.Theme.theme; + +final class AdminsData { + + private AdminsData() {} + + static List filterStaff(List in) { + List out = new ArrayList<>(in.size()); + for (StaffData.StaffEntry entry : in) { + if (entry.status() == StaffTracker.Status.OFFLINE && entry.offlineSince() <= 0) continue; + if (entry.status() == StaffTracker.Status.WAITING && entry.lastGone() <= 0) continue; + out.add(entry); + } + return out; + } + + static int resolveStatusColor(Admins host, StaffTracker.Status status) { + return switch (status) { + case GM0 -> 0xFF55FF55; + case GM1 -> 0xFF55DDFF; + case GM2 -> 0xFFFFFF55; + case GM3 -> 0xFFFF55FF; + case VANISH -> 0xFFFF7755; + case WAITING -> 0xFFAAAAAA; + case OFFLINE -> 0xFF888888; + default -> host.uiMuted != 0 ? host.uiMuted : 0xFF888888; + }; + } + + static String formatStatus(StaffData.StaffEntry entry, long now) { + return switch (entry.status()) { + case OFFLINE -> entry.offlineSince() <= 0 ? "--:--" : formatTime(now - entry.offlineSince()); + case WAITING -> entry.lastGone() <= 0 ? "--:--" : formatTime(now - entry.lastGone()); + case VANISH -> "Vanish"; + default -> entry.status().name(); + }; + } + + static String formatTime(long ms) { + if (ms < 0) return "--:--"; + long sec = ms / 1000L; + long min = sec / 60L; + long hrs = min / 60L; + if (hrs == 0) return String.format("%02d:%02d", min, sec % 60L); + return String.format("%02d:%02d:%02d", hrs, min % 60L, sec % 60L); + } + + static float measureLegacyName(TextRenderer renderer, Component displayName) { + Component converted = LegacyTextUtil.convertLegacyCodes(displayName); + List parts = TextRenderUtil.flattenStyled(converted, theme().textPrimary()); + float out = 0.0f; + for (Part part : parts) { + out += (float) renderer.getWidth(part.text(), false); + } + return out; + } + + static List> legacyParts(TextRenderer renderer, + Component displayName, + float alpha, + int fallbackColor) { + Component converted = LegacyTextUtil.convertLegacyCodes(displayName); + List parts = TextRenderUtil.flattenStyled(converted, fallbackColor); + List> out = new ArrayList<>(parts.size()); + float cursorX = 0.0f; + for (Part part : parts) { + int color = dev.loki.lovisual.features.gui.hud.config.HudRenderUtil.scaleAlpha(part.color(), alpha); + out.add(dev.loki.lovisual.features.gui.hud.draggable.impl.scripted.ScriptedListHudPanel.textPart(part.text(), color, cursorX)); + cursorX += (float) renderer.getWidth(part.text(), false); + } + return out; + } + + static AbstractClientPlayer resolvePlayerByName(String name) { + Minecraft mc = Minecraft.getInstance(); + if (name == null || name.isEmpty() || mc == null || mc.level == null) return null; + for (Player player : mc.level.players()) { + if (player instanceof AbstractClientPlayer clientPlayer) { + String playerName = clientPlayer.getGameProfile() != null + ? clientPlayer.getGameProfile().name() + : clientPlayer.getName().getString(); + if (playerName != null && playerName.equalsIgnoreCase(name)) return clientPlayer; + } + } + return null; + } + + static Identifier resolveSkinByName(String name) { + AbstractClientPlayer player = resolvePlayerByName(name); + if (player != null) return PlayerSkinResolver.resolveProfileSkin(player.getGameProfile()); + Minecraft mc = Minecraft.getInstance(); + if (mc != null && mc.getConnection() != null) { + for (PlayerInfo entry : mc.getConnection().getOnlinePlayers()) { + var profile = entry.getProfile(); + if (profile == null || profile.name() == null) continue; + if (!profile.name().equalsIgnoreCase(name)) continue; + Identifier skin = PlayerSkinResolver.resolveProfileSkin(profile); + if (skin != null) return skin; + } + } + return null; + } + + static Identifier resolvePreviewSkin() { + Minecraft mc = Minecraft.getInstance(); + if (mc != null && mc.player != null) { + return PlayerSkinResolver.resolveProfileSkin(mc.player.getGameProfile()); + } + return null; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/admins/AdminsPalette.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/admins/AdminsPalette.java new file mode 100644 index 0000000..468e824 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/admins/AdminsPalette.java @@ -0,0 +1,82 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.status; + +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; + +import static dev.loki.lovisual.features.theme.Theme.theme; + +final class AdminsPalette { + + private AdminsPalette() {} + + static void updatePalette(Admins host) { + if (isThemeMode(host)) { + int alpha = host.bgAlpha.get(); + int window = HudRenderUtil.setAlpha(theme().windowBg(), alpha); + int header = HudRenderUtil.setAlpha( + HudRenderUtil.mixColor(theme().windowHeader(), theme().surface(), 0.18f), + Math.min(255, alpha + 14) + ); + int surface = HudRenderUtil.setAlpha( + HudRenderUtil.mixColor(theme().surface(), theme().windowBg(), 0.22f), + Math.min(255, alpha + 6) + ); + int deep = HudRenderUtil.setAlpha( + HudRenderUtil.mixColor(theme().surface(), theme().windowHeader(), 0.42f), + Math.min(255, alpha + 18) + ); + host.uiHeaderLeft = HudRenderUtil.mixColor(header, window, 0.22f); + host.uiHeaderRight = HudRenderUtil.mixColor(surface, header, 0.52f); + host.uiBodyLeft = HudRenderUtil.mixColor(window, surface, 0.24f); + host.uiBodyRight = HudRenderUtil.mixColor(deep, surface, 0.18f); + host.uiOutline = HudRenderUtil.setAlpha( + HudRenderUtil.mixColor(theme().windowStroke(), theme().strokeSoft(), 0.18f), + Math.min(255, Math.max(182, alpha + 36)) + ); + host.uiText = theme().textPrimary(); + host.uiTitleText = HudRenderUtil.mixColor(theme().textPrimary(), theme().accent(), 0.14f); + host.uiMuted = HudRenderUtil.mixColor(theme().textMuted(), theme().textPrimary(), 0.16f); + host.uiCounter = HudRenderUtil.mixColor(theme().accent(), theme().textPrimary(), 0.38f); + host.uiDivider = HudRenderUtil.scaleAlpha(host.uiMuted, 0.68f); + host.uiBlurTint = HudRenderUtil.mixColor( + HudRenderUtil.mixColor(host.uiHeaderRight, host.uiBodyRight, 0.5f), + theme().accent(), + 0.14f + ); + return; + } + host.uiHeaderLeft = host.bg.getArgb(); + host.uiHeaderRight = host.bg2.getArgb(); + host.uiBodyLeft = HudRenderUtil.mixColor(host.bg.getArgb(), host.bg2.getArgb(), 0.15f); + host.uiBodyRight = host.bg2.getArgb(); + host.uiOutline = host.stroke.getArgb(); + host.uiText = host.text.getArgb(); + host.uiTitleText = host.uiText; + host.uiMuted = host.muted.getArgb(); + host.uiCounter = HudRenderUtil.mixColor(host.text.getArgb(), 0xFFE1E1FF, 0.20f); + host.uiDivider = HudRenderUtil.scaleAlpha(host.uiMuted, 0.55f); + host.uiBlurTint = HudRenderUtil.mixColor(host.uiHeaderLeft, host.uiBodyRight, 0.5f); + } + + static int withAlpha(int rgb, int alpha) { + return (AdminsAnimator.clamp255(alpha) << 24) | (rgb & 0x00FFFFFF); + } + + static boolean isThemeMode(Admins host) { + return Admins.COLOR_THEME.equals(host.colorMode.get()); + } + + static boolean isCustomMode(Admins host) { + return Admins.COLOR_CUSTOM.equals(host.colorMode.get()); + } + + static boolean hasEffect(Admins host) { + return !Admins.EFFECT_NONE.equals(host.bgEffect.get()); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/admins/AdminsRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/admins/AdminsRenderer.java new file mode 100644 index 0000000..f2755ab --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/admins/AdminsRenderer.java @@ -0,0 +1,133 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.status; + +import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; +import dev.loki.lovisual.features.gui.hud.draggable.impl.HudPanelLayoutModes; +import dev.loki.lovisual.features.gui.hud.draggable.impl.scripted.ScriptedListHudPanel; +import dev.loki.lovisual.features.relations.StaffData; +import dev.loki.lovisual.features.relations.StaffTracker; +import dev.loki.lovisual.render.engine.math.HudScale; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.font.Fonts; +import dev.loki.lovisual.render.engine.text.font.info.FontInfo; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.util.Util; + +import java.util.LinkedHashMap; +import java.util.List; + +final class AdminsRenderer { + + private AdminsRenderer() {} + + static void render(Admins host, Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, + float tickDelta, int screenW, int screenH) { + boolean forceVisible = dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry.isForceVisible(); + net.minecraft.client.Minecraft mc = net.minecraft.client.Minecraft.getInstance(); + if (mc == null || (mc.player == null && !forceVisible)) { host.setEmpty(); return; } + boolean chatPreview = dev.loki.lovisual.util.screen.ClientScreen.current() instanceof net.minecraft.client.gui.screens.ChatScreen; + List entries = AdminsData.filterStaff(StaffData.INSTANCE.getEntries()); + List animatedEntries = AdminsAnimator.animateEntries(host, entries); + boolean showExampleRow = entries.isEmpty() && (forceVisible || chatPreview); + boolean showWidget = !entries.isEmpty() || showExampleRow; + host.visibilityAnim = HudRenderUtil.animateVisibility(host.visibilityAnim, showWidget); + float widgetScale = HudRenderUtil.visibilityScale(host.visibilityAnim); + if (animatedEntries.isEmpty() && !showExampleRow && host.visibilityAnim <= 0.0f) { host.setEmpty(); return; } + AdminsPalette.updatePalette(host); + TextRenderer headerIconRenderer = Fonts.renderer("IconsNur", FontInfo.Type.Regular, TextRenderer.get()); + TextRenderer headerTextRenderer = Fonts.renderer("Inter", FontInfo.Type.Bold, TextRenderer.get()); + TextRenderer rowTextRenderer = Fonts.renderer("OnestMedium", FontInfo.Type.Regular, TextRenderer.get()); + if (headerTextRenderer == null) headerTextRenderer = textRenderer; + if (rowTextRenderer == null) rowTextRenderer = textRenderer; + if (headerIconRenderer == null) headerIconRenderer = textRenderer; + float baseScale = HudScale.scale(screenW, screenH) * 1.1f * HudPanelLayoutModes.effectiveScale(host.scaleValue); + float rowStep = Admins.ROW_STEP * baseScale; + float fontScale = 0.98f * (host.hud.getFontSize() / 18.0f); + headerIconRenderer.begin(fontScale, true, false); + float headerIconH = (float) headerIconRenderer.getHeight(false); + headerIconRenderer.end(); + headerTextRenderer.begin(fontScale, true, false); + float headerTextH = (float) headerTextRenderer.getHeight(false); + float titleWidth = (float) headerTextRenderer.getWidth("Admins", false); + headerTextRenderer.end(); + long now = System.currentTimeMillis(); + rowTextRenderer.begin(fontScale, true, false); + float rowTextH = (float) rowTextRenderer.getHeight(false); + float maxWidth = Admins.MIN_WIDTH * baseScale; + if (showExampleRow) { + float previewNameWidth = (float) rowTextRenderer.getWidth("Example Staff", false); + float previewStatusWidth = (float) rowTextRenderer.getWidth("Vanish", false); + maxWidth = Math.max(maxWidth, previewNameWidth + previewStatusWidth + (30.0f * baseScale)); + } else { + for (Admins.AnimatedEntry ae : animatedEntries) { + float nameWidth = AdminsData.measureLegacyName(rowTextRenderer, ae.entry().displayName()); + float statusWidth = (float) rowTextRenderer.getWidth(AdminsData.formatStatus(ae.entry(), now), false); + maxWidth = Math.max(maxWidth, nameWidth + statusWidth + (30.0f * baseScale)); + } + } + rowTextRenderer.end(); + rowTextRenderer.begin(fontScale * 0.92f, false, false); + String activeCountText = Integer.toString(entries.size()); + float activeLabelWidth = (float) rowTextRenderer.getWidth("Active:", false); + float activeCountWidth = (float) rowTextRenderer.getWidth(activeCountText, false); + rowTextRenderer.end(); + float headerWidth = (Admins.TITLE_TEXT_X * baseScale) + titleWidth + activeLabelWidth + activeCountWidth + ((Admins.COUNT_LABEL_OFFSET + 12.0f) * baseScale); + maxWidth = Math.max(maxWidth, headerWidth); + float contentHeight = showExampleRow ? rowStep : AdminsAnimator.totalAnimatedHeight(animatedEntries, rowStep); + float targetHeight = (showExampleRow || !animatedEntries.isEmpty()) ? (Admins.CONTENT_START_Y * baseScale + contentHeight) : (Admins.HEADER_HEIGHT * baseScale); + host.displayWidth = HudRenderUtil.animateDimension(host.displayWidth, maxWidth); + host.displayHeight = HudRenderUtil.animateDimension(host.displayHeight, targetHeight); + host.setSize(host.displayWidth, host.displayHeight); + if (!showWidget && widgetScale <= 0.001f && animatedEntries.isEmpty()) { host.setEmpty(); return; } + float drawScale = Math.max(0.0f, widgetScale); + float drawWidth = host.displayWidth * drawScale; + float drawHeight = host.displayHeight * drawScale; + if (drawWidth <= 0.0f || drawHeight <= 0.0f) { host.setEmpty(); return; } + float drawX = host.getX() + ((host.displayWidth - drawWidth) * 0.5f); + float drawY = host.getY() + ((host.displayHeight - drawHeight) * 0.5f); + float drawBaseScale = baseScale * drawScale; + float drawFontScale = fontScale * drawScale; + float drawHeaderIconH = headerIconH * drawScale; + float drawHeaderTextH = headerTextH * drawScale; + float drawRowTextH = rowTextH * drawScale; + int resolvedHeaderIconColor = host.uiCounter; + if (host.headerIconPulse.get()) { + int pulsed = HudTextEffects.animatedColor(host.uiCounter, HudTextEffects.Effect.PULSE, host.headerIconPulseSpeed.get(), (float) (Util.getMillis() / 1000.0), 0.0f); + resolvedHeaderIconColor = HudRenderUtil.mixColor(host.uiCounter, pulsed, host.headerIconPulseIntensity.get() / 100.0f); + } + List> panelRows = ScriptedListHudPanel.rows(); + rowTextRenderer.begin(drawFontScale, false, false); + if (showExampleRow) { + panelRows.add(ScriptedListHudPanel.row("preview", "head", ScriptedListHudPanel.idString(AdminsData.resolvePreviewSkin()), + List.of(ScriptedListHudPanel.textPart("Example Staff", AdminsPalette.withAlpha(host.uiText & 0x00FFFFFF, 255), 0.0f)), + "Vanish", AdminsPalette.withAlpha(AdminsData.resolveStatusColor(host, StaffTracker.Status.VANISH) & 0x00FFFFFF, 255), + AdminsPalette.withAlpha(host.uiMuted & 0x00FFFFFF, 130), 1.0f)); + } else { + for (Admins.AnimatedEntry ae : animatedEntries) { + float anim = ae.anim(); if (anim <= 0.01f) continue; + StaffData.StaffEntry entry = ae.entry(); + int rowAlpha = AdminsAnimator.clamp255(Math.round(255.0f * anim)); + int nameBaseColor = AdminsPalette.withAlpha(host.uiText & 0x00FFFFFF, rowAlpha); + List> parts = AdminsData.legacyParts(rowTextRenderer, entry.displayName(), anim, nameBaseColor); + panelRows.add(ScriptedListHudPanel.row(entry.name(), "head", ScriptedListHudPanel.idString(AdminsData.resolveSkinByName(entry.name())), parts, + AdminsData.formatStatus(entry, now), AdminsPalette.withAlpha(AdminsData.resolveStatusColor(host, entry.status()) & 0x00FFFFFF, rowAlpha), + AdminsPalette.withAlpha(host.uiMuted & 0x00FFFFFF, Math.max(26, rowAlpha - 120)), anim)); + } + } + rowTextRenderer.end(); + host.scriptedPanel.render(renderer, textRenderer, ctx, tickDelta, new ScriptedListHudPanel.Panel(ScriptedListHudPanel.ADMINS, + new ScriptedListHudPanel.Palette(host.uiHeaderLeft, host.uiHeaderRight, host.uiBodyLeft, host.uiBodyRight, host.uiOutline, host.uiText, host.uiMuted, host.uiCounter, host.uiTitleText, host.uiDivider, host.uiBlurTint), + drawX, drawY, drawWidth, drawHeight, drawScale, drawBaseScale, drawFontScale, drawHeaderIconH, drawHeaderTextH, drawRowTextH, + activeLabelWidth * drawScale, activeCountWidth * drawScale, entries.size(), + AdminsPalette.hasEffect(host), Math.min(1.0f, (host.blurAlpha.get() / 255.0f) * (AdminsPalette.isThemeMode(host) ? 1.15f : 1.0f)), + resolvedHeaderIconColor, HudPanelLayoutModes.current(host.layoutMode), panelRows)); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/armor/ArmorData.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/armor/ArmorData.java new file mode 100644 index 0000000..0466576 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/armor/ArmorData.java @@ -0,0 +1,65 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.status; + +import dev.loki.lovisual.features.gui.hud.config.HudGlobalConfig; +import dev.loki.lovisual.util.screen.ClientScreen; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.ChatScreen; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; + +final class ArmorData { + + private ArmorData() {} + + static boolean hasAnyStacks(ItemStack[] stacks) { + if (stacks == null) return false; + for (ItemStack stack : stacks) { + if (stack != null && !stack.isEmpty()) { + return true; + } + } + return false; + } + + static ItemStack[] getArmorStacks() { + Minecraft mc = Minecraft.getInstance(); + return new ItemStack[]{ + mc.player.getItemBySlot(EquipmentSlot.HEAD), + mc.player.getItemBySlot(EquipmentSlot.CHEST), + mc.player.getItemBySlot(EquipmentSlot.LEGS), + mc.player.getItemBySlot(EquipmentSlot.FEET) + }; + } + + static ItemStack[] previewStacks() { + ItemStack head = new ItemStack(Items.DIAMOND_HELMET); + ItemStack chest = new ItemStack(Items.DIAMOND_CHESTPLATE); + ItemStack legs = new ItemStack(Items.DIAMOND_LEGGINGS); + ItemStack feet = new ItemStack(Items.DIAMOND_BOOTS); + head.setDamageValue(Math.max(1, head.getMaxDamage() / 3)); + chest.setDamageValue(Math.max(1, chest.getMaxDamage() / 2)); + legs.setDamageValue(Math.max(1, legs.getMaxDamage() / 4)); + feet.setDamageValue(Math.max(1, feet.getMaxDamage() / 5)); + return new ItemStack[]{head, chest, legs, feet}; + } + + static boolean shouldShowWhenEmpty(Armor host) { + Minecraft mc = Minecraft.getInstance(); + HudGlobalConfig hud = host.hud; + if (hud == null) return false; + HudGlobalConfig.HeaderMode mode = hud.getHeaderMode(); + if (mode == HudGlobalConfig.HeaderMode.ALWAYS) return true; + if (mode == HudGlobalConfig.HeaderMode.CHAT) { + return mc != null && ClientScreen.current() instanceof ChatScreen; + } + return false; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/armor/ArmorRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/armor/ArmorRenderer.java new file mode 100644 index 0000000..589a4af --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/armor/ArmorRenderer.java @@ -0,0 +1,148 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.status; + +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; +import dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry; +import dev.loki.lovisual.render.engine.math.HudScale; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.world.item.ItemStack; + +final class ArmorRenderer { + + private ArmorRenderer() {} + + static void render(Armor host, Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, + float tickDelta, int screenW, int screenH) { + Minecraft mc = Minecraft.getInstance(); + host.layoutReady = false; + boolean preview = DraggableHudElementRegistry.isForceVisible(); + float scale = HudScale.scale(screenW, screenH) * host.scaleValue.get().floatValue(); + boolean horizontal = host.isHorizontal(); + + host.slotSize = Armor.DEFAULT_SLOT_SIZE * scale; + host.slotGap = Armor.DEFAULT_SLOT_GAP * scale; + host.slotRadius = Armor.PANEL_RADIUS * scale; + host.itemScale = scale; + + float w = horizontal ? Armor.SLOTS * host.slotSize + (Armor.SLOTS - 1) * host.slotGap : host.slotSize; + float h = horizontal ? host.slotSize : Armor.SLOTS * host.slotSize + (Armor.SLOTS - 1) * host.slotGap; + host.setArmorSize(w, h); + + if (mc == null || (mc.player == null && !preview)) { + host.setEmptySize(); + return; + } + if (!preview && !host.isEnabled()) { + host.setEmptySize(); + return; + } + + ItemStack[] stacks = (mc != null && mc.player != null) ? ArmorData.getArmorStacks() : new ItemStack[0]; + if (preview && !ArmorData.hasAnyStacks(stacks)) { + stacks = ArmorData.previewStacks(); + } + host.cachedStacks = stacks; + if (!ArmorData.hasAnyStacks(host.cachedStacks) && !ArmorData.shouldShowWhenEmpty(host) && !preview) { + host.setEmptySize(); + return; + } + + boolean reverse = host.getX() > screenW * 0.5f; + host.renderX = reverse ? (host.getX() - host.getWidth()) : host.getX(); + host.renderY = host.getY(); + host.baseX = host.renderX; + host.baseY = host.renderY; + + float glassScale = Armor.PANEL_RADIUS <= 0.0f ? 1.0f : host.slotRadius / Armor.PANEL_RADIUS; + HudRenderUtil.drawLiquidGlass( + host.baseX, + host.baseY, + host.getWidth(), + host.getHeight(), + host.slotRadius, + glassScale, + true, + Armor.GLASS_BLUR_STRENGTH, + Armor.GLASS_ALPHA + ); + renderer.roundedRectCorners( + host.baseX, + host.baseY, + host.getWidth(), + host.getHeight(), + host.slotRadius, + host.slotRadius, + host.slotRadius, + host.slotRadius, + 1.0f, + HudRenderUtil.glassSmallBackground(Armor.GLASS_ALPHA) + ); + drawSlotDividers(host, renderer, horizontal, scale); + + host.layoutReady = true; + } + + static void renderForeground(Armor host, Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, + float tickDelta, int screenW, int screenH) { + Minecraft mc = Minecraft.getInstance(); + if (!host.layoutReady || mc == null || mc.player == null || ctx == null) return; + + ItemStack[] stacks = host.cachedStacks != null ? host.cachedStacks : ArmorData.getArmorStacks(); + float iconSize = 16f * host.itemScale; + float offset = (host.slotSize - iconSize) * 0.5f; + boolean horizontal = host.isHorizontal(); + boolean reverseOrder = host.isReverseOrder(); + Armor.StateMode mode = host.stateMode.get(); + int overlayFlags = mode == Armor.StateMode.TEXT + ? Renderer2D.ITEM_OVERLAY_DURABILITY_TEXT + : Renderer2D.ITEM_OVERLAY_DURABILITY; + int durabilityThreshold = mode == Armor.StateMode.TEXT ? host.stateThreshold.get() : host.stateBarThreshold.get(); + int durabilityColorThreshold = mode == Armor.StateMode.TEXT ? host.stateColorThreshold.get() : host.stateBarThreshold.get(); + + int seed = 0; + for (int i = 0; i < stacks.length; i++) { + int idx = reverseOrder ? (Armor.SLOTS - 1 - i) : i; + ItemStack stack = stacks[idx]; + if (stack == null || stack.isEmpty()) continue; + float sx = host.baseX + (horizontal ? i * (host.slotSize + host.slotGap) : 0f) + offset; + float sy = host.baseY + (horizontal ? 0f : i * (host.slotSize + host.slotGap)) + offset; + + // Armor widget is laid out in UNSCALED_LOGICAL coordinates together with its slots. + renderer.item(stack, sx, sy, host.itemScale, seed++, overlayFlags, null, + durabilityThreshold, durabilityColorThreshold); + } + } + + static void drawSlotDividers(Armor host, Renderer2D renderer, boolean horizontal, float scale) { + if (renderer == null) return; + float dividerThickness = Math.max(0.5f, Armor.DIVIDER_THICKNESS * scale); + float dividerLength = Math.max(6.0f * scale, host.slotSize * 0.52f); + float dividerInset = Math.max(0.0f, (host.slotSize - dividerLength) * 0.5f); + int divider = HudRenderUtil.scaleAlpha(0xFFFFFFFF, 0.12f); + int dividerShadow = HudRenderUtil.scaleAlpha(0xFF000000, 0.10f); + for (int i = 0; i < Armor.SLOTS - 1; i++) { + if (horizontal) { + float lineX = host.baseX + host.slotSize + i * (host.slotSize + host.slotGap) + + host.slotGap * 0.5f - dividerThickness * 0.5f; + float lineY = host.baseY + dividerInset; + renderer.quad(lineX + dividerThickness, lineY, dividerThickness, dividerLength, dividerShadow); + renderer.quad(lineX, lineY, dividerThickness, dividerLength, divider); + } else { + float lineX = host.baseX + dividerInset; + float lineY = host.baseY + host.slotSize + i * (host.slotSize + host.slotGap) + + host.slotGap * 0.5f - dividerThickness * 0.5f; + renderer.quad(lineX, lineY + dividerThickness, dividerLength, dividerThickness, dividerShadow); + renderer.quad(lineX, lineY, dividerLength, dividerThickness, divider); + } + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/cooldowns/CooldownsAnimator.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/cooldowns/CooldownsAnimator.java new file mode 100644 index 0000000..c186f38 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/cooldowns/CooldownsAnimator.java @@ -0,0 +1,64 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.status; + +import dev.loki.lovisual.render.engine.animation.AnimationUtility; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +final class CooldownsAnimator { + + private CooldownsAnimator() {} + + static List animateRows(Cooldowns host, List rows) { + Map current = new LinkedHashMap<>(); + for (Cooldowns.Row row : rows) current.put(row.key(), row); + Map merged = new LinkedHashMap<>(); + merged.putAll(current); + for (Map.Entry entry : host.lastEntries.entrySet()) merged.putIfAbsent(entry.getKey(), entry.getValue()); + host.lastEntries.clear(); + host.lastEntries.putAll(merged); + List toRemove = new ArrayList<>(); + for (Map.Entry entry : host.lastEntries.entrySet()) { + String key = entry.getKey(); + float target = current.containsKey(key) ? 1.0f : 0.0f; + float anim = host.entryAnim.getOrDefault(key, 0.0f); + anim = AnimationUtility.approach(anim, target, AnimationUtility.deltaTime(), 12.0f); + anim = AnimationUtility.snap(anim, target, 0.02f); + if (anim <= 0.0f && target <= 0.0f) toRemove.add(key); + else host.entryAnim.put(key, anim); + } + for (String key : toRemove) { host.entryAnim.remove(key); host.lastEntries.remove(key); } + List out = new ArrayList<>(); + for (Map.Entry entry : host.lastEntries.entrySet()) { + float anim = host.entryAnim.getOrDefault(entry.getKey(), 0.0f); + if (anim > 0.01f) out.add(new Cooldowns.AnimatedRow(entry.getValue(), anim)); + } + return out; + } + + static float totalAnimatedHeight(List rows, float rowStep) { + float out = 0.0f; + for (Cooldowns.AnimatedRow row : rows) out += rowStep * row.anim(); + return out; + } + + static int getBlinkAlpha(Cooldowns host, Cooldowns.Row row, int baseAlpha, int minAlphaFloor) { + if (!host.blink.get()) return baseAlpha; + if (row.remainingTicks() <= 0 || row.remainingTicks() > Cooldowns.BLINK_THRESHOLD_TICKS) return baseAlpha; + double wave = Math.sin(System.currentTimeMillis() * Cooldowns.BLINK_SPEED); + float factor = (float) ((wave + 1.0) * 0.5); + int minAlpha = Math.max(0, Math.min(baseAlpha, minAlphaFloor)); + return clamp255(Math.round(minAlpha + (baseAlpha - minAlpha) * (1.0f - factor))); + } + + static int clamp255(int value) { return Math.max(0, Math.min(255, value)); } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/cooldowns/CooldownsData.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/cooldowns/CooldownsData.java new file mode 100644 index 0000000..8da14c9 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/cooldowns/CooldownsData.java @@ -0,0 +1,105 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.status; + +import dev.loki.lovisual.features.module.lifecycle.Modules; +import dev.loki.lovisual.features.module.modules.combat.pvp.PvpCooldowns; +import dev.loki.lovisual.mixins.accessors.inventory.ItemCooldownEntryAccessor; +import dev.loki.lovisual.mixins.accessors.inventory.ItemCooldownManagerAccessor; +import dev.loki.lovisual.util.pvp.client.CooldownsState; +import dev.loki.lovisual.util.pvp.cooldown.CooldownRegistry; +import dev.loki.lovisual.util.pvp.cooldown.ItemCooldownSnapshot; +import dev.loki.lovisual.util.pvp.state.PvpState; +import net.minecraft.client.Minecraft; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.Identifier; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; + +import java.util.ArrayList; +import java.util.List; + +import static dev.loki.lovisual.features.theme.Theme.theme; + +final class CooldownsData { + + private CooldownsData() {} + + static List collectRows(Cooldowns host) { + Minecraft mc = Minecraft.getInstance(); + List rows = new ArrayList<>(); + if (mc != null && mc.player != null) { + var manager = mc.player.getCooldowns(); + int nowTick = ((ItemCooldownManagerAccessor) manager).getTick(); + ((ItemCooldownManagerAccessor) manager).getEntries().forEach((Identifier id, Object entry) -> { + int remainingTicks = ((ItemCooldownEntryAccessor) entry).getEndTick() - nowTick; + if (remainingTicks <= 0) return; + Item item = id != null ? BuiltInRegistries.ITEM.getValue(id) : Items.AIR; + ItemStack stack = new ItemStack(item); + String name = stack.isEmpty() ? (id != null ? id.getPath() : "Unknown") : stack.getHoverName().getString(); + String key = "vanilla:" + (id != null ? id.toString() : name); + rows.add(new Cooldowns.Row(key, stack, name, formatSeconds(remainingTicks / 20.0f), remainingTicks, host.uiMuted != 0 ? host.uiMuted : theme().textMuted())); + }); + PvpCooldowns pvpCooldowns = Modules.get(PvpCooldowns.class); + boolean pvpEnabled = pvpCooldowns != null && pvpCooldowns.isSystemEnabled(); + if (pvpEnabled && pvpCooldowns.shouldRenderWidget()) { + boolean inPvp = CooldownsState.MANAGER.isInPvp(); + int stateColor = inPvp ? Cooldowns.PVP_ACTIVE_COLOR : Cooldowns.PVP_IDLE_COLOR; + for (Item item : CooldownRegistry.trackedItems()) { + ItemCooldownSnapshot snapshot = CooldownsState.MANAGER.snapshot(item); + if (!snapshot.visible()) continue; + float seconds = snapshot.cooling() ? snapshot.cooldownRemainingMs() / 1000.0f : snapshot.useWindowRemainingMs() / 1000.0f; + String text = snapshot.cooling() ? formatSeconds(seconds) : snapshot.compactText(); + int remainingTicks = snapshot.cooling() ? Math.max(1, Math.round(seconds * 20.0f)) : Math.max(1, Math.round(Math.max(1.0f, seconds) * 20.0f)); + ItemStack stack = new ItemStack(item); + rows.add(new Cooldowns.Row("pvp:" + BuiltInRegistries.ITEM.getKey(item), stack, stack.getHoverName().getString(), text, remainingTicks, snapshot.cooling() ? stateColor : (host.uiMuted != 0 ? host.uiMuted : theme().textMuted()))); + } + } + if (pvpEnabled && PvpState.isActive()) { + Float secondsLeft = PvpState.getSecondsLeft(); + if (secondsLeft != null && secondsLeft > 0.0f) { + int remainingTicks = Math.max(1, Math.round(secondsLeft * 20.0f)); + ItemStack pearlStack = new ItemStack(Items.ENDER_PEARL); + rows.add(new Cooldowns.Row("pvpban:" + BuiltInRegistries.ITEM.getKey(Items.ENDER_PEARL), pearlStack, pearlStack.getHoverName().getString(), formatSeconds(secondsLeft), remainingTicks, Cooldowns.PVP_BLOCKED_COLOR)); + ItemStack chorusStack = new ItemStack(Items.CHORUS_FRUIT); + rows.add(new Cooldowns.Row("pvpban:" + BuiltInRegistries.ITEM.getKey(Items.CHORUS_FRUIT), chorusStack, chorusStack.getHoverName().getString(), formatSeconds(secondsLeft), remainingTicks, Cooldowns.PVP_BLOCKED_COLOR)); + } + } + } + rows.sort((a, b) -> String.CASE_INSENSITIVE_ORDER.compare(a.name(), b.name())); + return rows; + } + + static String formatSeconds(float seconds) { + if (seconds >= 10.0f) return String.format("%.0fs", seconds); + return String.format("%.1fs", Math.round(seconds * 10.0f) / 10.0f); + } + + static ItemStack[] previewStacks(Cooldowns host) { + if (Cooldowns.previewStacks == null || Cooldowns.previewStacks.length != Cooldowns.PREVIEW_ITEMS.length) { + Cooldowns.previewStacks = buildPreviewStacks(); + } + return Cooldowns.previewStacks; + } + + private static ItemStack[] buildPreviewStacks() { + ItemStack[] stacks = new ItemStack[Cooldowns.PREVIEW_ITEMS.length]; + for (int i = 0; i < Cooldowns.PREVIEW_ITEMS.length; i++) stacks[i] = new ItemStack(Cooldowns.PREVIEW_ITEMS[i]); + return stacks; + } + + static void queueItemIcon(List itemTasks, ItemStack stack, float iconX, float centerY, float baseScale, int seed) { + if (itemTasks == null || stack.isEmpty()) return; + float iconSize = Cooldowns.ROW_ICON_VISUAL_SIZE * baseScale; + float itemScale = iconSize / 16.0f; + float itemDrawX = iconX; + float itemDrawY = centerY - (16.0f * itemScale * 0.5f); + itemTasks.add(new Cooldowns.QueuedItemIcon(stack.copy(), itemDrawX, itemDrawY, itemScale, seed)); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/cooldowns/CooldownsPalette.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/cooldowns/CooldownsPalette.java new file mode 100644 index 0000000..34d970e --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/cooldowns/CooldownsPalette.java @@ -0,0 +1,56 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.status; + +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; + +import static dev.loki.lovisual.features.theme.Theme.theme; + +final class CooldownsPalette { + + private CooldownsPalette() {} + + static void updatePalette(Cooldowns host) { + if (isThemeMode(host)) { + int alpha = host.bgAlpha.get(); + int window = HudRenderUtil.setAlpha(theme().windowBg(), alpha); + int header = HudRenderUtil.setAlpha(HudRenderUtil.mixColor(theme().windowHeader(), theme().surface(), 0.18f), Math.min(255, alpha + 14)); + int surface = HudRenderUtil.setAlpha(HudRenderUtil.mixColor(theme().surface(), theme().windowBg(), 0.22f), Math.min(255, alpha + 6)); + int deep = HudRenderUtil.setAlpha(HudRenderUtil.mixColor(theme().surface(), theme().windowHeader(), 0.42f), Math.min(255, alpha + 18)); + host.uiHeaderLeft = HudRenderUtil.mixColor(header, window, 0.22f); + host.uiHeaderRight = HudRenderUtil.mixColor(surface, header, 0.52f); + host.uiBodyLeft = HudRenderUtil.mixColor(window, surface, 0.24f); + host.uiBodyRight = HudRenderUtil.mixColor(deep, surface, 0.18f); + host.uiOutline = HudRenderUtil.setAlpha(HudRenderUtil.mixColor(theme().windowStroke(), theme().strokeSoft(), 0.18f), Math.min(255, Math.max(182, alpha + 36))); + host.uiText = theme().textPrimary(); + host.uiTitleText = HudRenderUtil.mixColor(theme().textPrimary(), theme().accent(), 0.14f); + host.uiMuted = HudRenderUtil.mixColor(theme().textMuted(), theme().textPrimary(), 0.16f); + host.uiCounter = HudRenderUtil.mixColor(theme().accent(), theme().textPrimary(), 0.38f); + host.uiDivider = HudRenderUtil.scaleAlpha(host.uiMuted, 0.68f); + host.uiBlurTint = HudRenderUtil.mixColor(HudRenderUtil.mixColor(host.uiHeaderRight, host.uiBodyRight, 0.5f), theme().accent(), 0.14f); + return; + } + host.uiHeaderLeft = host.bg.getArgb(); + host.uiHeaderRight = host.bg2.getArgb(); + host.uiBodyLeft = HudRenderUtil.mixColor(host.bg.getArgb(), host.bg2.getArgb(), 0.15f); + host.uiBodyRight = host.bg2.getArgb(); + host.uiOutline = host.stroke.getArgb(); + host.uiText = host.text.getArgb(); + host.uiTitleText = host.uiText; + host.uiMuted = host.muted.getArgb(); + host.uiCounter = HudRenderUtil.mixColor(host.text.getArgb(), 0xFFE1E1FF, 0.20f); + host.uiDivider = HudRenderUtil.scaleAlpha(host.uiMuted, 0.55f); + host.uiBlurTint = HudRenderUtil.mixColor(host.uiHeaderLeft, host.uiBodyRight, 0.5f); + } + + static int withAlpha(int rgb, int alpha) { return (CooldownsAnimator.clamp255(alpha) << 24) | (rgb & 0x00FFFFFF); } + + static boolean isThemeMode(Cooldowns host) { return Cooldowns.COLOR_THEME.equals(host.colorMode.get()); } + static boolean isCustomMode(Cooldowns host) { return Cooldowns.COLOR_CUSTOM.equals(host.colorMode.get()); } + static boolean hasEffect(Cooldowns host) { return !Cooldowns.EFFECT_NONE.equals(host.bgEffect.get()); } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/cooldowns/CooldownsRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/cooldowns/CooldownsRenderer.java new file mode 100644 index 0000000..59a6cc0 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/cooldowns/CooldownsRenderer.java @@ -0,0 +1,142 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.status; + +import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; +import dev.loki.lovisual.features.gui.hud.draggable.impl.HudPanelLayoutModes; +import dev.loki.lovisual.features.gui.hud.draggable.impl.scripted.ScriptedListHudPanel; +import dev.loki.lovisual.render.engine.color.RenderColor; +import dev.loki.lovisual.render.engine.math.HudScale; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.font.Fonts; +import dev.loki.lovisual.render.engine.text.font.info.FontInfo; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.util.Util; +import net.minecraft.world.item.ItemStack; + +import java.util.LinkedHashMap; +import java.util.List; + +final class CooldownsRenderer { + + private CooldownsRenderer() {} + + static void render(Cooldowns host, Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta, int screenW, int screenH) { + net.minecraft.client.Minecraft mc = net.minecraft.client.Minecraft.getInstance(); + host.foregroundReady = false; + host.pendingItemTasks.clear(); + boolean forceVisible = dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry.isForceVisible(); + if (mc == null || (mc.player == null && !forceVisible)) { host.setEmpty(); return; } + boolean chatPreview = dev.loki.lovisual.util.screen.ClientScreen.current() instanceof net.minecraft.client.gui.screens.ChatScreen; + List rows = CooldownsData.collectRows(host); + List animatedRows = CooldownsAnimator.animateRows(host, rows); + boolean showExampleRow = rows.isEmpty() && (forceVisible || chatPreview); + boolean showWidget = !rows.isEmpty() || showExampleRow; + host.visibilityAnim = HudRenderUtil.animateVisibility(host.visibilityAnim, showWidget); + float widgetScale = HudRenderUtil.visibilityScale(host.visibilityAnim); + if (animatedRows.isEmpty() && !showExampleRow && host.visibilityAnim <= 0.0f) { host.setEmpty(); return; } + CooldownsPalette.updatePalette(host); + TextRenderer headerIconRenderer = Fonts.renderer("IconsNur", FontInfo.Type.Regular, TextRenderer.get()); + TextRenderer headerTextRenderer = Fonts.renderer("Inter", FontInfo.Type.Bold, TextRenderer.get()); + TextRenderer rowTextRenderer = Fonts.renderer("OnestMedium", FontInfo.Type.Regular, TextRenderer.get()); + if (headerTextRenderer == null) headerTextRenderer = textRenderer; + if (rowTextRenderer == null) rowTextRenderer = textRenderer; + if (headerIconRenderer == null) headerIconRenderer = textRenderer; + float baseScale = HudScale.scale(screenW, screenH) * 1.1f * HudPanelLayoutModes.effectiveScale(host.scaleValue); + float rowStep = Cooldowns.ROW_STEP * baseScale; + float fontScale = 0.98f * (host.hud.getFontSize() / 18.0f); + headerIconRenderer.begin(fontScale, true, false); + float headerIconH = (float) headerIconRenderer.getHeight(false); + headerIconRenderer.end(); + headerTextRenderer.begin(fontScale, true, false); + float headerTextH = (float) headerTextRenderer.getHeight(false); + float titleWidth = (float) headerTextRenderer.getWidth("Cooldowns", false); + headerTextRenderer.end(); + rowTextRenderer.begin(fontScale, true, false); + float rowTextH = (float) rowTextRenderer.getHeight(false); + float maxWidth = Cooldowns.MIN_WIDTH * baseScale; + if (showExampleRow) { + float previewWidth = (float) rowTextRenderer.getWidth("Example Cooldowns**:**", false) + (30.0f * baseScale); + maxWidth = Math.max(maxWidth, previewWidth); + } else { + for (Cooldowns.AnimatedRow ar : animatedRows) { + Cooldowns.Row row = ar.row(); + float w = (float) rowTextRenderer.getWidth(row.name() + row.duration(), false) + (30.0f * baseScale); + maxWidth = Math.max(maxWidth, w); + } + } + rowTextRenderer.end(); + rowTextRenderer.begin(fontScale * 0.92f, false, false); + String activeCountText = Integer.toString(rows.size()); + float activeLabelWidth = (float) rowTextRenderer.getWidth("Active:", false); + float activeCountWidth = (float) rowTextRenderer.getWidth(activeCountText, false); + rowTextRenderer.end(); + float headerWidth = (Cooldowns.TITLE_TEXT_X * baseScale) + titleWidth + activeLabelWidth + activeCountWidth + ((Cooldowns.COUNT_LABEL_OFFSET + 12.0f) * baseScale); + maxWidth = Math.max(maxWidth, headerWidth); + float contentHeight = showExampleRow ? rowStep : CooldownsAnimator.totalAnimatedHeight(animatedRows, rowStep); + float targetHeight = (showExampleRow || !animatedRows.isEmpty()) ? (Cooldowns.CONTENT_START_Y * baseScale + contentHeight) : (Cooldowns.HEADER_HEIGHT * baseScale); + host.displayWidth = HudRenderUtil.animateDimension(host.displayWidth, maxWidth); + host.displayHeight = HudRenderUtil.animateDimension(host.displayHeight, targetHeight); + host.setSize(host.displayWidth, host.displayHeight); + if (!showWidget && widgetScale <= 0.001f && animatedRows.isEmpty()) { host.setEmpty(); return; } + float drawScale = Math.max(0.0f, widgetScale); + float drawWidth = host.displayWidth * drawScale; + float drawHeight = host.displayHeight * drawScale; + if (drawWidth <= 0.0f || drawHeight <= 0.0f) { host.setEmpty(); return; } + float drawX = host.getX() + ((host.displayWidth - drawWidth) * 0.5f); + float drawY = host.getY() + ((host.displayHeight - drawHeight) * 0.5f); + float drawBaseScale = baseScale * drawScale; + float drawFontScale = fontScale * drawScale; + float drawHeaderIconH = headerIconH * drawScale; + float drawHeaderTextH = headerTextH * drawScale; + float drawRowTextH = rowTextH * drawScale; + int resolvedHeaderIconColor = host.uiCounter; + if (host.headerIconPulse.get()) { + int pulsed = HudTextEffects.animatedColor(host.uiCounter, HudTextEffects.Effect.PULSE, host.headerIconPulseSpeed.get(), (float) (Util.getMillis() / 1000.0), 0.0f); + resolvedHeaderIconColor = HudRenderUtil.mixColor(host.uiCounter, pulsed, host.headerIconPulseIntensity.get() / 100.0f); + } + List> panelRows = ScriptedListHudPanel.rows(); + rowTextRenderer.begin(drawFontScale, false, false); + if (showExampleRow) { + ItemStack stack = CooldownsData.previewStacks(host)[(int) ((System.currentTimeMillis() / 1000L) % CooldownsData.previewStacks(host).length)]; + panelRows.add(ScriptedListHudPanel.row("preview", "item", BuiltInRegistries.ITEM.getKey(stack.getItem()).toString(), List.of(ScriptedListHudPanel.textPart("Example Cooldowns", CooldownsPalette.withAlpha(host.uiText & 0x00FFFFFF, 255), 0.0f)), "**:**", CooldownsPalette.withAlpha(host.uiCounter & 0x00FFFFFF, 255), CooldownsPalette.withAlpha(host.uiMuted & 0x00FFFFFF, 130), 1.0f)); + } else { + for (Cooldowns.AnimatedRow ar : animatedRows) { + float anim = ar.anim(); if (anim <= 0.01f) continue; + Cooldowns.Row rowData = ar.row(); + int rowBaseAlpha = CooldownsAnimator.clamp255(Math.round(255.0f * anim)); + int iconAlpha = CooldownsAnimator.getBlinkAlpha(host, rowData, rowBaseAlpha, 110); + int textAlpha = CooldownsAnimator.getBlinkAlpha(host, rowData, rowBaseAlpha, 95); + int timeAlpha = CooldownsAnimator.getBlinkAlpha(host, rowData, rowBaseAlpha, 60); + panelRows.add(ScriptedListHudPanel.row(rowData.key(), "item", BuiltInRegistries.ITEM.getKey(rowData.stack().getItem()).toString(), List.of(ScriptedListHudPanel.textPart(rowData.name(), CooldownsPalette.withAlpha(host.uiText & 0x00FFFFFF, textAlpha), 0.0f)), rowData.duration(), CooldownsPalette.withAlpha(rowData.timeColor() & 0x00FFFFFF, timeAlpha), CooldownsPalette.withAlpha(host.uiMuted & 0x00FFFFFF, Math.max(26, iconAlpha - 120)), anim)); + } + } + rowTextRenderer.end(); + host.scriptedPanel.render(renderer, textRenderer, ctx, tickDelta, new ScriptedListHudPanel.Panel(ScriptedListHudPanel.COOLDOWNS, new ScriptedListHudPanel.Palette(host.uiHeaderLeft, host.uiHeaderRight, host.uiBodyLeft, host.uiBodyRight, host.uiOutline, host.uiText, host.uiMuted, host.uiCounter, host.uiTitleText, host.uiDivider, host.uiBlurTint), drawX, drawY, drawWidth, drawHeight, drawScale, drawBaseScale, drawFontScale, drawHeaderIconH, drawHeaderTextH, drawRowTextH, activeLabelWidth * drawScale, activeCountWidth * drawScale, rows.size(), CooldownsPalette.hasEffect(host), Math.min(1.0f, (host.blurAlpha.get() / 255.0f) * (CooldownsPalette.isThemeMode(host) ? 1.15f : 1.0f)), resolvedHeaderIconColor, HudPanelLayoutModes.current(host.layoutMode), panelRows)); + host.foregroundReady = false; + } + + static void renderExampleRow(Cooldowns host, Renderer2D renderer, TextRenderer rowTextRenderer, float fontScale, float rowTextH, float baseScale, float renderWidth, float centerY, float renderX) { + ItemStack stack = CooldownsData.previewStacks(host)[(int) ((System.currentTimeMillis() / 1000L) % CooldownsData.previewStacks(host).length)]; + int textColor = CooldownsPalette.withAlpha(host.uiText & 0x00FFFFFF, 255); + int timeColor = CooldownsPalette.withAlpha(host.uiCounter & 0x00FFFFFF, 255); + int dividerColor = CooldownsPalette.withAlpha(host.uiMuted & 0x00FFFFFF, 130); + float rowContentCenterY = centerY + (Cooldowns.ROW_CONTENT_CENTER_OFFSET * baseScale); + CooldownsData.queueItemIcon(host.pendingItemTasks, stack, renderX + (Cooldowns.ROW_ICON_X * baseScale), rowContentCenterY, baseScale, 0); + renderer.quad(renderX + (Cooldowns.ROW_DIVIDER_X * baseScale), rowContentCenterY - ((Cooldowns.ROW_DIVIDER_H * baseScale) * 0.5f), Math.max(0.5f, 0.5f * baseScale), Cooldowns.ROW_DIVIDER_H * baseScale, dividerColor); + rowTextRenderer.begin(fontScale, false, false); + float textY = rowContentCenterY - (rowTextH * 0.5f); + rowTextRenderer.render("Example Cooldowns", renderX + (Cooldowns.ROW_TEXT_X * baseScale), textY, new RenderColor(textColor), false); + float durationWidth = (float) rowTextRenderer.getWidth("**:**", false); + rowTextRenderer.render("**:**", renderX + renderWidth - durationWidth - (Cooldowns.ROW_TIME_RIGHT * baseScale), textY, new RenderColor(timeColor), false); + rowTextRenderer.end(); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/potions/PotionsAnimator.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/potions/PotionsAnimator.java new file mode 100644 index 0000000..0dbcb85 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/potions/PotionsAnimator.java @@ -0,0 +1,92 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.status; + +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import net.minecraft.world.effect.MobEffectCategory; +import net.minecraft.world.effect.MobEffectInstance; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +final class PotionsAnimator { + + private PotionsAnimator() {} + + static List animateRows(Potions host, List rows) { + Map current = new LinkedHashMap<>(); + for (Potions.Row row : rows) { + current.put(row.key(), row); + } + Map merged = new LinkedHashMap<>(); + merged.putAll(current); + for (Map.Entry entry : host.lastEntries.entrySet()) { + merged.putIfAbsent(entry.getKey(), entry.getValue()); + } + host.lastEntries.clear(); + host.lastEntries.putAll(merged); + List toRemove = new ArrayList<>(); + for (Map.Entry entry : host.lastEntries.entrySet()) { + String key = entry.getKey(); + float target = current.containsKey(key) ? 1.0f : 0.0f; + float anim = host.entryAnim.getOrDefault(key, 0.0f); + anim = AnimationUtility.approach(anim, target, AnimationUtility.deltaTime(), 12.0f); + anim = AnimationUtility.snap(anim, target, 0.02f); + if (anim <= 0.0f && target <= 0.0f) { + toRemove.add(key); + } else { + host.entryAnim.put(key, anim); + } + } + for (String key : toRemove) { + host.entryAnim.remove(key); + host.lastEntries.remove(key); + } + List out = new ArrayList<>(); + for (Map.Entry entry : host.lastEntries.entrySet()) { + float anim = host.entryAnim.getOrDefault(entry.getKey(), 0.0f); + if (anim > 0.01f) { + out.add(new Potions.AnimatedRow(entry.getValue(), anim)); + } + } + return out; + } + + static float totalAnimatedHeight(List rows, float rowStep) { + float out = 0.0f; + for (Potions.AnimatedRow row : rows) { + out += rowStep * row.anim(); + } + return out; + } + + static int getBlinkAlpha(Potions.Row row, int baseAlpha, int minAlphaFloor) { + MobEffectInstance inst = row.effect(); + if (inst == null) { + return baseAlpha; + } + int duration = inst.getDuration(); + if (duration <= 0 || duration > Potions.BLINK_THRESHOLD_TICKS) { + return baseAlpha; + } + double wave = Math.sin(System.currentTimeMillis() * Potions.BLINK_SPEED); + float factor = (float) ((wave + 1.0) * 0.5); + int minAlpha = Math.max(0, Math.min(baseAlpha, minAlphaFloor)); + return clamp255(Math.round(minAlpha + (baseAlpha - minAlpha) * (1.0f - factor))); + } + + static boolean isBadEffect(MobEffectInstance effect) { + return effect != null && effect.getEffect().value().getCategory() == MobEffectCategory.HARMFUL; + } + + static int clamp255(int value) { + return Math.max(0, Math.min(255, value)); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/potions/PotionsData.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/potions/PotionsData.java new file mode 100644 index 0000000..2d0dd02 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/potions/PotionsData.java @@ -0,0 +1,54 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.status; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.resources.language.I18n; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.effect.MobEffectInstance; + +import java.util.ArrayList; +import java.util.List; + +final class PotionsData { + + private PotionsData() {} + + static List collectRows() { + List rows = new ArrayList<>(); + Minecraft mc = Minecraft.getInstance(); + if (mc != null && mc.player != null) { + mc.player.getActiveEffects().stream() + .sorted((a, b) -> String.CASE_INSENSITIVE_ORDER.compare(I18n.get(a.getDescriptionId()), I18n.get(b.getDescriptionId()))) + .forEach(inst -> { + String name = I18n.get(inst.getDescriptionId()); + String amp = inst.getAmplifier() > 0 ? Integer.toString(inst.getAmplifier() + 1) : ""; + String duration = formatDuration(inst); + Identifier effectId = inst.getEffect().unwrapKey().map(ResourceKey::identifier).orElse(null); + String key = effectId != null ? effectId.toString() : name; + Identifier iconId = effectId != null ? Identifier.fromNamespaceAndPath("minecraft", "textures/mob_effect/" + effectId.getPath() + ".png") : null; + rows.add(new Potions.Row(key, inst, name, amp, duration, iconId)); + }); + } + return rows; + } + + static String formatDuration(MobEffectInstance inst) { + int rawTicks = inst.getDuration(); + if (rawTicks < 0) { + return "**:**"; + } + int ticks = Math.max(0, rawTicks); + int minutes = ticks / 1200; + if (minutes > 60) { + return "**:**"; + } + return minutes + ":" + String.format("%02d", (ticks % 1200) / 20); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/potions/PotionsPalette.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/potions/PotionsPalette.java new file mode 100644 index 0000000..7cc877b --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/potions/PotionsPalette.java @@ -0,0 +1,68 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.status; + +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; + +import static dev.loki.lovisual.features.theme.Theme.theme; + +final class PotionsPalette { + + private PotionsPalette() {} + + static void updatePalette(Potions host) { + if (isThemeMode(host)) { + int alpha = host.bgAlpha.get(); + int window = HudRenderUtil.setAlpha(theme().windowBg(), alpha); + int header = HudRenderUtil.setAlpha(HudRenderUtil.mixColor(theme().windowHeader(), theme().surface(), 0.18f), Math.min(255, alpha + 14)); + int surface = HudRenderUtil.setAlpha(HudRenderUtil.mixColor(theme().surface(), theme().windowBg(), 0.22f), Math.min(255, alpha + 6)); + int deep = HudRenderUtil.setAlpha(HudRenderUtil.mixColor(theme().surface(), theme().windowHeader(), 0.42f), Math.min(255, alpha + 18)); + host.uiHeaderLeft = HudRenderUtil.mixColor(header, window, 0.22f); + host.uiHeaderRight = HudRenderUtil.mixColor(surface, header, 0.52f); + host.uiBodyLeft = HudRenderUtil.mixColor(window, surface, 0.24f); + host.uiBodyRight = HudRenderUtil.mixColor(deep, surface, 0.18f); + host.uiOutline = HudRenderUtil.setAlpha(HudRenderUtil.mixColor(theme().windowStroke(), theme().strokeSoft(), 0.18f), Math.min(255, Math.max(182, alpha + 36))); + host.uiText = theme().textPrimary(); + host.uiTitleText = HudRenderUtil.mixColor(theme().textPrimary(), theme().accent(), 0.14f); + host.uiMuted = HudRenderUtil.mixColor(theme().textMuted(), theme().textPrimary(), 0.16f); + host.uiCounter = HudRenderUtil.mixColor(theme().accent(), theme().textPrimary(), 0.38f); + host.uiAmpText = HudRenderUtil.mixColor(theme().textPrimary(), theme().accent(), 0.22f); + host.uiDivider = HudRenderUtil.scaleAlpha(host.uiMuted, 0.68f); + host.uiBlurTint = HudRenderUtil.mixColor(HudRenderUtil.mixColor(host.uiHeaderRight, host.uiBodyRight, 0.5f), theme().accent(), 0.14f); + return; + } + host.uiHeaderLeft = host.bg.getArgb(); + host.uiHeaderRight = host.bg2.getArgb(); + host.uiBodyLeft = HudRenderUtil.mixColor(host.bg.getArgb(), host.bg2.getArgb(), 0.15f); + host.uiBodyRight = host.bg2.getArgb(); + host.uiOutline = host.stroke.getArgb(); + host.uiText = host.text.getArgb(); + host.uiTitleText = host.uiText; + host.uiMuted = host.muted.getArgb(); + host.uiCounter = HudRenderUtil.mixColor(host.text.getArgb(), 0xFFE1E1FF, 0.20f); + host.uiAmpText = host.uiText; + host.uiDivider = HudRenderUtil.scaleAlpha(host.uiMuted, 0.55f); + host.uiBlurTint = HudRenderUtil.mixColor(host.uiHeaderLeft, host.uiBodyRight, 0.5f); + } + + static int withAlpha(int rgb, int alpha) { + return (PotionsAnimator.clamp255(alpha) << 24) | (rgb & 0x00FFFFFF); + } + + static boolean isThemeMode(Potions host) { + return Potions.COLOR_THEME.equals(host.colorMode.get()); + } + + static boolean isCustomMode(Potions host) { + return Potions.COLOR_CUSTOM.equals(host.colorMode.get()); + } + + static boolean hasEffect(Potions host) { + return !Potions.EFFECT_NONE.equals(host.bgEffect.get()); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/potions/PotionsRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/potions/PotionsRenderer.java new file mode 100644 index 0000000..f2304d6 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/status/potions/PotionsRenderer.java @@ -0,0 +1,166 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.draggable.impl.status; + +import dev.loki.lovisual.features.gui.hud.base.HudTextEffects; +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; +import dev.loki.lovisual.features.gui.hud.draggable.impl.HudPanelLayoutModes; +import dev.loki.lovisual.features.gui.hud.draggable.impl.scripted.ScriptedListHudPanel; +import dev.loki.lovisual.render.engine.color.RenderColor; +import dev.loki.lovisual.render.engine.math.HudScale; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.font.Fonts; +import dev.loki.lovisual.render.engine.text.font.info.FontInfo; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.resources.Identifier; +import net.minecraft.util.Util; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; + +final class PotionsRenderer { + + private PotionsRenderer() {} + + static void render(Potions host, Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta, int screenW, int screenH) { + boolean forceVisible = dev.loki.lovisual.features.gui.hud.draggable.DraggableHudElementRegistry.isForceVisible(); + net.minecraft.client.Minecraft mc = net.minecraft.client.Minecraft.getInstance(); + if (mc == null || (mc.player == null && !forceVisible)) { + host.setEmpty(); + return; + } + boolean chatPreview = dev.loki.lovisual.util.screen.ClientScreen.current() instanceof net.minecraft.client.gui.screens.ChatScreen; + List rows = PotionsData.collectRows(); + List animatedRows = PotionsAnimator.animateRows(host, rows); + boolean showExampleRow = rows.isEmpty() && (forceVisible || chatPreview); + boolean showWidget = !rows.isEmpty() || showExampleRow; + host.visibilityAnim = HudRenderUtil.animateVisibility(host.visibilityAnim, showWidget); + float widgetScale = HudRenderUtil.visibilityScale(host.visibilityAnim); + if (animatedRows.isEmpty() && !showExampleRow && host.visibilityAnim <= 0.0f) { + host.setEmpty(); + return; + } + PotionsPalette.updatePalette(host); + TextRenderer headerIconRenderer = Fonts.renderer("IconsNur", FontInfo.Type.Regular, TextRenderer.get()); + TextRenderer headerTextRenderer = Fonts.renderer("Inter", FontInfo.Type.Bold, TextRenderer.get()); + TextRenderer rowTextRenderer = Fonts.renderer("OnestMedium", FontInfo.Type.Regular, TextRenderer.get()); + if (headerTextRenderer == null) headerTextRenderer = textRenderer; + if (rowTextRenderer == null) rowTextRenderer = textRenderer; + if (headerIconRenderer == null) headerIconRenderer = textRenderer; + float baseScale = HudScale.scale(screenW, screenH) * 1.1f * HudPanelLayoutModes.effectiveScale(host.scaleValue); + float rowStep = Potions.ROW_STEP * baseScale; + float fontScale = 0.98f * (host.hud.getFontSize() / 18.0f); + headerIconRenderer.begin(fontScale, true, false); + float headerIconH = (float) headerIconRenderer.getHeight(false); + headerIconRenderer.end(); + headerTextRenderer.begin(fontScale, true, false); + float headerTextH = (float) headerTextRenderer.getHeight(false); + float titleWidth = (float) headerTextRenderer.getWidth("Potions", false); + headerTextRenderer.end(); + rowTextRenderer.begin(fontScale, true, false); + float rowTextH = (float) rowTextRenderer.getHeight(false); + float maxWidth = Potions.MIN_WIDTH * baseScale; + if (showExampleRow) { + float previewWidth = (float) rowTextRenderer.getWidth("Example effect**:**", false) + (30.0f * baseScale); + maxWidth = Math.max(maxWidth, previewWidth); + } else { + for (Potions.AnimatedRow animatedRow : animatedRows) { + Potions.Row row = animatedRow.row(); + String label = row.name() + (row.amp().isEmpty() ? "" : " " + row.amp()); + float widthCandidate = (float) rowTextRenderer.getWidth(label + row.duration(), false) + (30.0f * baseScale); + maxWidth = Math.max(maxWidth, widthCandidate); + } + } + rowTextRenderer.end(); + rowTextRenderer.begin(fontScale * 0.92f, false, false); + String activeCountText = Integer.toString(rows.size()); + float activeLabelWidth = (float) rowTextRenderer.getWidth("Active:", false); + float activeCountWidth = (float) rowTextRenderer.getWidth(activeCountText, false); + rowTextRenderer.end(); + float headerWidth = (Potions.TITLE_TEXT_X * baseScale) + titleWidth + activeLabelWidth + activeCountWidth + ((Potions.COUNT_LABEL_OFFSET + 12.0f) * baseScale); + maxWidth = Math.max(maxWidth, headerWidth); + float contentHeight = showExampleRow ? rowStep : PotionsAnimator.totalAnimatedHeight(animatedRows, rowStep); + float targetHeight = (showExampleRow || !animatedRows.isEmpty()) ? (Potions.CONTENT_START_Y * baseScale + contentHeight) : (Potions.HEADER_HEIGHT * baseScale); + host.displayWidth = HudRenderUtil.animateDimension(host.displayWidth, maxWidth); + host.displayHeight = HudRenderUtil.animateDimension(host.displayHeight, targetHeight); + host.setSize(host.displayWidth, host.displayHeight); + if (!showWidget && widgetScale <= 0.001f && animatedRows.isEmpty()) { + host.setEmpty(); + return; + } + float drawScale = Math.max(0.0f, widgetScale); + float drawWidth = host.displayWidth * drawScale; + float drawHeight = host.displayHeight * drawScale; + if (drawWidth <= 0.0f || drawHeight <= 0.0f) { + host.setEmpty(); + return; + } + float drawX = host.getX() + ((host.displayWidth - drawWidth) * 0.5f); + float drawY = host.getY() + ((host.displayHeight - drawHeight) * 0.5f); + float drawBaseScale = baseScale * drawScale; + float drawFontScale = fontScale * drawScale; + float drawHeaderIconH = headerIconH * drawScale; + float drawHeaderTextH = headerTextH * drawScale; + float drawRowTextH = rowTextH * drawScale; + int resolvedHeaderIconColor = host.uiCounter; + if (host.headerIconPulse.get()) { + int pulsedHeaderIconColor = HudTextEffects.animatedColor(host.uiCounter, HudTextEffects.Effect.PULSE, host.headerIconPulseSpeed.get(), (float) (Util.getMillis() / 1000.0), 0.0f); + resolvedHeaderIconColor = HudRenderUtil.mixColor(host.uiCounter, pulsedHeaderIconColor, host.headerIconPulseIntensity.get() / 100.0f); + } + List> panelRows = ScriptedListHudPanel.rows(); + rowTextRenderer.begin(drawFontScale, false, false); + if (showExampleRow) { + Identifier previewIcon = Potions.PREVIEW_ICONS[(int) ((System.currentTimeMillis() / 1000L) % Potions.PREVIEW_ICONS.length)]; + List> parts = List.of(ScriptedListHudPanel.textPart("Example effect", PotionsPalette.withAlpha(host.uiText & 0x00FFFFFF, 255), 0.0f)); + LinkedHashMap row = ScriptedListHudPanel.row("preview", "texture", ScriptedListHudPanel.idString(previewIcon), parts, "**:**", PotionsPalette.withAlpha(host.uiCounter & 0x00FFFFFF, 255), PotionsPalette.withAlpha(host.uiMuted & 0x00FFFFFF, 130), 1.0f); + row.put("iconTint", ScriptedListHudPanel.hex(PotionsPalette.withAlpha(0x00FFFFFF, 255))); + panelRows.add(row); + } else { + for (Potions.AnimatedRow animatedRow : animatedRows) { + float anim = animatedRow.anim(); + if (anim <= 0.01f) continue; + Potions.Row rowData = animatedRow.row(); + int rowBaseAlpha = PotionsAnimator.clamp255(Math.round(255.0f * anim)); + int blinkAlpha = PotionsAnimator.getBlinkAlpha(rowData, rowBaseAlpha, 100); + boolean bad = PotionsAnimator.isBadEffect(rowData.effect()); + int nameColor = PotionsPalette.withAlpha(bad ? Potions.NEGATIVE_EFFECT_RGB : (host.uiText & 0x00FFFFFF), blinkAlpha); + int dividerColor = PotionsPalette.withAlpha((bad ? Potions.NEGATIVE_EFFECT_RGB : (host.uiMuted & 0x00FFFFFF)), Math.max(24, blinkAlpha - 125)); + List> parts = new ArrayList<>(2); + parts.add(ScriptedListHudPanel.textPart(rowData.name(), nameColor, 0.0f)); + if (!rowData.amp().isEmpty()) { + float labelWidth = (float) rowTextRenderer.getWidth(rowData.name(), false); + parts.add(ScriptedListHudPanel.textPart(" " + rowData.amp(), PotionsPalette.withAlpha(host.uiAmpText & 0x00FFFFFF, blinkAlpha), labelWidth)); + } + LinkedHashMap row = ScriptedListHudPanel.row(rowData.key(), "texture", ScriptedListHudPanel.idString(rowData.iconId()), parts, rowData.duration(), PotionsPalette.withAlpha(host.uiCounter & 0x00FFFFFF, blinkAlpha), dividerColor, anim); + row.put("iconTint", ScriptedListHudPanel.hex(PotionsPalette.withAlpha(0x00FFFFFF, blinkAlpha))); + panelRows.add(row); + } + } + rowTextRenderer.end(); + host.scriptedPanel.render(renderer, textRenderer, ctx, tickDelta, new ScriptedListHudPanel.Panel(ScriptedListHudPanel.POTIONS, new ScriptedListHudPanel.Palette(host.uiHeaderLeft, host.uiHeaderRight, host.uiBodyLeft, host.uiBodyRight, host.uiOutline, host.uiText, host.uiMuted, host.uiCounter, host.uiTitleText, host.uiDivider, host.uiBlurTint), drawX, drawY, drawWidth, drawHeight, drawScale, drawBaseScale, drawFontScale, drawHeaderIconH, drawHeaderTextH, drawRowTextH, activeLabelWidth * drawScale, activeCountWidth * drawScale, rows.size(), PotionsPalette.hasEffect(host), Math.min(1.0f, (host.blurAlpha.get() / 255.0f) * (PotionsPalette.isThemeMode(host) ? 1.15f : 1.0f)), resolvedHeaderIconColor, HudPanelLayoutModes.current(host.layoutMode), panelRows)); + } + + static void renderExampleRow(Potions host, Renderer2D renderer, TextRenderer rowTextRenderer, float fontScale, float rowTextH, float baseScale, float renderWidth, float centerY, float renderX) { + int textColor = PotionsPalette.withAlpha(host.uiText & 0x00FFFFFF, 255); + int timeColor = PotionsPalette.withAlpha(host.uiCounter & 0x00FFFFFF, 255); + int dividerColor = PotionsPalette.withAlpha(host.uiMuted & 0x00FFFFFF, 130); + float iconSize = Potions.ROW_ICON_SIZE * baseScale; + float rowContentCenterY = centerY + (Potions.ROW_CONTENT_CENTER_OFFSET * baseScale); + Identifier previewIcon = Potions.PREVIEW_ICONS[(int) ((System.currentTimeMillis() / 1000L) % Potions.PREVIEW_ICONS.length)]; + Renderer2D.TEXTURE.roundedTexRect(renderX + (Potions.ROW_ICON_X * baseScale), rowContentCenterY - (iconSize * 0.5f), iconSize, iconSize, Math.min(1.15f * baseScale, iconSize * 0.18f), 1.0f, PotionsPalette.withAlpha(0x00FFFFFF, 255), previewIcon); + renderer.quad(renderX + (Potions.ROW_DIVIDER_X * baseScale), rowContentCenterY - ((Potions.ROW_DIVIDER_H * baseScale) * 0.5f), Math.max(0.5f, 0.5f * baseScale), Potions.ROW_DIVIDER_H * baseScale, dividerColor); + rowTextRenderer.begin(fontScale, false, false); + float textY = rowContentCenterY - (rowTextH * 0.5f); + rowTextRenderer.render("Example effect", renderX + (Potions.ROW_TEXT_X * baseScale), textY, new RenderColor(textColor), false); + float durationWidth = (float) rowTextRenderer.getWidth("**:**", false); + rowTextRenderer.render("**:**", renderX + renderWidth - durationWidth - (Potions.ROW_TIME_RIGHT * baseScale), textY, new RenderColor(timeColor), false); + rowTextRenderer.end(); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/TargetHud.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/TargetHud.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/TargetHud.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/TargetHud.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/TargetHudFormatter.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/TargetHudFormatter.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/TargetHudFormatter.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/TargetHudFormatter.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/TargetHudModel.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/TargetHudModel.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/TargetHudModel.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/TargetHudModel.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/TargetHudTargetResolver.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/TargetHudTargetResolver.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/TargetHudTargetResolver.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/TargetHudTargetResolver.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/TargetHudContentRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/TargetHudContentRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/TargetHudContentRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/TargetHudContentRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/TargetHudPanelRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/TargetHudPanelRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/TargetHudPanelRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/TargetHudPanelRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/TargetHudRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/TargetHudRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/TargetHudRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/TargetHudRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/TargetHudTextRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/TargetHudTextRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/TargetHudTextRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/TargetHudTextRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/parts/TargetHudArcRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/parts/TargetHudArcRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/parts/TargetHudArcRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/parts/TargetHudArcRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/parts/TargetHudEffectsRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/parts/TargetHudEffectsRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/parts/TargetHudEffectsRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/parts/TargetHudEffectsRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/parts/TargetHudEquipmentRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/parts/TargetHudEquipmentRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/parts/TargetHudEquipmentRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/parts/TargetHudEquipmentRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/parts/TargetHudFaceRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/parts/TargetHudFaceRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/parts/TargetHudFaceRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/targethud/render/parts/TargetHudFaceRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/TrackedEye.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/TrackedEye.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/TrackedEye.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/TrackedEye.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/TriangulatorSolver.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/TriangulatorSolver.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/TriangulatorSolver.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/TriangulatorSolver.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/TriangulatorTracker.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/TriangulatorTracker.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/TriangulatorTracker.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/TriangulatorTracker.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/TriangulatorView.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/TriangulatorView.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/TriangulatorView.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/TriangulatorView.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/model/CapturePolicy.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/model/CapturePolicy.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/model/CapturePolicy.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/model/CapturePolicy.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/model/DisplayState.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/model/DisplayState.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/model/DisplayState.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/model/DisplayState.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/render/TriangulatorMeasure.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/render/TriangulatorMeasure.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/render/TriangulatorMeasure.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/render/TriangulatorMeasure.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/render/TriangulatorPainter.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/render/TriangulatorPainter.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/render/TriangulatorPainter.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/render/TriangulatorPainter.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/interaction/RegistryDragController.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/interaction/RegistryDragController.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/interaction/RegistryDragController.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/interaction/RegistryDragController.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/interaction/RegistryDragSession.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/interaction/RegistryDragSession.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/interaction/RegistryDragSession.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/interaction/RegistryDragSession.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryAnchorLayout.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryAnchorLayout.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryAnchorLayout.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryAnchorLayout.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistrySnapSupport.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistrySnapSupport.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistrySnapSupport.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistrySnapSupport.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryState.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryState.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryState.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryState.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryWidgetStore.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryWidgetStore.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryWidgetStore.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/registry/RegistryWidgetStore.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryOverlayRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryOverlayRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryOverlayRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryOverlayRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryRenderPipeline.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryRenderPipeline.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryRenderPipeline.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryRenderPipeline.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryResizeHandler.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryResizeHandler.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryResizeHandler.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryResizeHandler.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryWidgetRenderPath.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryWidgetRenderPath.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryWidgetRenderPath.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/render/RegistryWidgetRenderPath.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/StaticHudElementBootstrap.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/StaticHudElementBootstrap.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/StaticHudElementBootstrap.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/StaticHudElementBootstrap.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/StaticHudElementRegistry.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/StaticHudElementRegistry.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/StaticHudElementRegistry.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/StaticHudElementRegistry.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/CustomBar.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/CustomBar.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/CustomBar.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/CustomBar.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/CustomBarModel.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/CustomBarModel.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/CustomBarModel.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/CustomBarModel.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/CustomHealthBar.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/CustomHealthBar.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/CustomHealthBar.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/CustomHealthBar.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/CustomXpBarRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/CustomXpBarRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/CustomXpBarRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/CustomXpBarRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/jump/CustomJumpBarRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/jump/CustomJumpBarRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/jump/CustomJumpBarRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/bars/jump/CustomJumpBarRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/BetterButtons.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/BetterButtons.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/BetterButtons.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/BetterButtons.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/SwapTooltip.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/SwapTooltip.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/SwapTooltip.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/SwapTooltip.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonSliderRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonSliderRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonSliderRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonSliderRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonSprites.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonSprites.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonSprites.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonSprites.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonStyleRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonStyleRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonStyleRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonStyleRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonTextRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonTextRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonTextRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonTextRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/state/ButtonHoverState.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/state/ButtonHoverState.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/state/ButtonHoverState.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/state/ButtonHoverState.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/state/ButtonParallax.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/state/ButtonParallax.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/state/ButtonParallax.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/state/ButtonParallax.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/state/ButtonQueue.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/state/ButtonQueue.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/state/ButtonQueue.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/state/ButtonQueue.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/DynamicIsland.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/DynamicIsland.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/DynamicIsland.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/DynamicIsland.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandBoundsPatches.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandBoundsPatches.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandBoundsPatches.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandBoundsPatches.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandPatchOps.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandPatchOps.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandPatchOps.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandPatchOps.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandRuntimePatches.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandRuntimePatches.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandRuntimePatches.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/IslandRuntimePatches.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/controls/IslandControls.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/controls/IslandControls.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/controls/IslandControls.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/controls/IslandControls.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/controls/IslandUiUtil.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/controls/IslandUiUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/controls/IslandUiUtil.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/dynamicisland/controls/IslandUiUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/hotbar/CustomHotbar.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/hotbar/CustomHotbar.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/hotbar/CustomHotbar.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/hotbar/CustomHotbar.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/CustomLocatorCollector.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/CustomLocatorCollector.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/CustomLocatorCollector.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/CustomLocatorCollector.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/CustomLocatorHeads.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/CustomLocatorHeads.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/CustomLocatorHeads.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/CustomLocatorHeads.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/CustomLocatorLabelCache.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/CustomLocatorLabelCache.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/CustomLocatorLabelCache.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/CustomLocatorLabelCache.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/CustomLocatorLabels.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/CustomLocatorLabels.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/CustomLocatorLabels.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/CustomLocatorLabels.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/panel/CustomLocatorLabelPanel.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/panel/CustomLocatorLabelPanel.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/panel/CustomLocatorLabelPanel.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/panel/CustomLocatorLabelPanel.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/panel/CustomLocatorMarkers.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/panel/CustomLocatorMarkers.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/panel/CustomLocatorMarkers.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/panel/CustomLocatorMarkers.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/panel/CustomLocatorRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/panel/CustomLocatorRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/panel/CustomLocatorRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/panel/CustomLocatorRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/panel/CustomLocatorResolver.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/panel/CustomLocatorResolver.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/panel/CustomLocatorResolver.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/locator/panel/CustomLocatorResolver.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/overlay/ReachDisplay.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/overlay/ReachDisplay.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/overlay/ReachDisplay.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/overlay/ReachDisplay.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/overlay/Watermark.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/overlay/Watermark.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/overlay/Watermark.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/overlay/Watermark.java diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/CustomTabList.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/CustomTabList.java new file mode 100644 index 0000000..99d23eb --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/CustomTabList.java @@ -0,0 +1,136 @@ +package dev.loki.lovisual.features.gui.hud.nondraggable.impl.tab; + +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import dev.loki.lovisual.config.values.color.RGBAColorValue; +import dev.loki.lovisual.config.values.color.RGBColorValue; +import dev.loki.lovisual.features.gui.hud.anchor.HudRenderSpace; +import dev.loki.lovisual.features.gui.hud.base.AbstractHudElement; +import dev.loki.lovisual.features.gui.hud.registry.HudElementRegister; +import dev.loki.lovisual.features.module.phase.HudPhase; +import dev.loki.lovisual.Lifecycle; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.core.context.ViewportContext; +import dev.loki.lovisual.render.engine.core.frame.RenderPhase; +import dev.loki.lovisual.render.engine.core.frame.RenderPhaseScope; +import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; +import dev.loki.lovisual.render.engine.math.HudScale; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.helpers.util.TickDelta; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.world.scores.DisplaySlot; +import net.minecraft.world.scores.Objective; +import net.minecraft.world.scores.Scoreboard; +import net.minecraft.util.Util; +import java.util.List; + +@HudElementRegister(order = 95) +public final class CustomTabList extends AbstractHudElement { + public static final CustomTabList INSTANCE = new CustomTabList(); + private static final float TOP_Y = 56f; + private static final long STRUCTURE_PROBE_INTERVAL_MS = 250L; + private static final long DYNAMIC_REFRESH_INTERVAL_MS = 500L; + private final TabListAnimator animator = new TabListAnimator(); + private final TabListPalette palette = new TabListPalette(); + private final NumberValue maxWidth = num("tab_list_max_width", "max_width", 1680, 760, 2600); + private final BooleanValue syncTheme = bool("tab_list_sync_theme", "sync_theme", true); + private final NumberValue themeAlpha = visibleWhen(num("tab_list_theme_alpha", "theme_alpha", 225, 0, 255), syncTheme::get); + private final RGBAColorValue bgColor = visibleWhen(color("tab_list_bg", "background", "#E50B1018"), () -> !syncTheme.get()); + private final RGBColorValue accentColor = visibleWhen(colorNoAlpha("tab_list_accent", "accent", "#6E8DFF"), () -> !syncTheme.get()); + private final RGBColorValue textColor = visibleWhen(colorNoAlpha("tab_list_text", "text", "#F4F7FB"), () -> !syncTheme.get()); + private final RGBColorValue mutedColor = visibleWhen(colorNoAlpha("tab_list_muted", "muted", "#9AA5B5"), () -> !syncTheme.get()); + private TabListModel.Snapshot snapshot = TabListModel.Snapshot.empty(); + private long lastStructureProbeMs; + private long lastDynamicRefreshMs; + private long lastStructureSignature = Long.MIN_VALUE; + private int lastSnapshotWidth = -1; + private boolean wasShown; + + private CustomTabList() { super("tab_list", "Tab List", true); } + + public static boolean shouldReplaceVanilla() { + return Lifecycle.canRunHud() && INSTANCE != null && INSTANCE.isEnabled() && INSTANCE.canRenderReplacement(); + } + public static void renderVanillaTabStratum(GuiGraphicsExtractor ctx, int guiWidth) { + CustomTabList i = INSTANCE; if (i == null) return; i.renderVanillaTabStratumInternal(ctx, guiWidth); + } + private void renderVanillaTabStratumInternal(GuiGraphicsExtractor ctx, int guiWidth) { + Minecraft mc = Minecraft.getInstance(); + if (!Lifecycle.canRunHud()) return; + if (mc == null || mc.getWindow() == null || ctx == null) return; + int screenW = Math.max(1, Math.round(HudScale.virtualWidth(mc.getWindow().getWidth(), mc.getWindow().getHeight()))); + int screenH = Math.max(1, Math.round(HudScale.virtualHeight(mc.getWindow().getWidth(), mc.getWindow().getHeight()))); + LoVisualRenderSystem.ensureFrameContext(); + LoVisualRenderSystem.updateFrameTiming(TickDelta.tickProgress(false), TickDelta.frameDeltaTicks(), TickDelta.fixedDeltaTicks()); + ViewportContext.beginCurrentStratumUnscaledLogical(ctx); + try (RenderPhaseScope ignored = LoVisualRenderSystem.phase(RenderPhase.SCREEN_TOP, "2d:vanilla_stratum:tab_list")) { + Renderer2D.COLOR.begin(); renderTab(Renderer2D.COLOR, ctx, TickDelta.tickProgress(false), screenW, screenH, true); Renderer2D.COLOR.render(); + } finally { ViewportContext.endCurrentStratum(ctx); } + } + @Override public boolean usesEngineRenderer() { return true; } + @Override public HudRenderSpace getRenderSpace() { return HudRenderSpace.UNSCALED_LOGICAL; } + @Override public HudPhase getHudPhase() { return HudPhase.NONE; } + @Override public int getRenderOrder() { return 1000; } + @Override public void renderEngineForeground(Renderer2D renderer, dev.loki.lovisual.render.engine.text.render.TextRenderer tr, GuiGraphicsExtractor ctx, float tickDelta, int screenW, int screenH) { + renderTab(renderer, ctx, tickDelta, screenW, screenH, isEnabled() && shouldShowNow()); + } + private void renderTab(Renderer2D renderer, GuiGraphicsExtractor ctx, float tickDelta, int screenW, int screenH, boolean show) { + Minecraft mc = Minecraft.getInstance(); + if (mc == null || ctx == null || renderer == null) return; + float panelPresence = animator.updatePanel(show); + if (!show && panelPresence <= 0.01f && !animator.hasVisibleRows()) { setBounds(0f, 0f, 0f, 0f); return; } + if (show) refreshSnapshot(screenW, !wasShown); + wasShown = show; animator.updateRows(show ? snapshot.visibleIds() : null); + float panelAlpha = AnimationUtility.easeOutCubic(panelPresence); + float width = tabPanelWidth(screenW); + float height = snapshot.height(); + float drawX = (screenW - width) * 0.5f; + float drawY = TOP_Y - (1f - panelAlpha) * 18f; + setBounds(drawX, drawY, width, height); + palette.update(syncTheme.get(), themeAlpha.get(), bgColor.getArgb(), accentColor.getArgb(), textColor.getArgb(), mutedColor.getArgb()); + if (panelAlpha <= 0.01f) return; + TabListShellRenderer.renderChrome(renderer, drawX, drawY, width, height, panelAlpha, snapshot, palette); + List rows = TabListRowRenderer.collect(drawX, drawY, width, panelAlpha, snapshot, animator, palette); + TabListRowRenderer.renderRows(renderer, rows, palette); + TabListTextRenderer.renderHeadsAndText(ctx, renderer, drawX, drawY, width, panelAlpha, snapshot, animator, palette); + } + private void refreshSnapshot(int screenW, boolean force) { + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.getConnection() == null) return; + long now = Util.getMillis(); + int listedPlayers = Math.min(mc.getConnection().getListedOnlinePlayers().size(), TabListModel.maxPlayers()); + boolean multiWide = listedPlayers > TabListModel.maxRowsPerColumn() * 2; + int effectiveWidth = Math.max(1, Math.min(screenW, multiWide ? screenW : maxWidth.get())); + boolean needCollect = force || snapshot.entries().isEmpty() || snapshot.entries().size() != listedPlayers || effectiveWidth != lastSnapshotWidth; + long signature = lastStructureSignature; + if (needCollect || now - lastStructureProbeMs >= STRUCTURE_PROBE_INTERVAL_MS) { + signature = TabListModel.structureSignature(mc, effectiveWidth); + lastStructureProbeMs = now; + if (signature != lastStructureSignature) needCollect = true; + } + if (needCollect) { snapshot = TabListModel.collect(mc, effectiveWidth); lastSnapshotWidth = effectiveWidth; lastStructureSignature = signature; lastDynamicRefreshMs = now; return; } + if (now - lastDynamicRefreshMs >= DYNAMIC_REFRESH_INTERVAL_MS) { snapshot = TabListModel.refreshDynamic(mc, snapshot); lastDynamicRefreshMs = now; } + } + private float tabPanelWidth(int screenW) { + float screenCap = Math.max(360f, screenW - 24f); + float configured = Math.max(1f, maxWidth.get()); + float wanted = snapshot.width(); + if (snapshot.columns() > 2) configured = Math.max(configured, wanted); + return Math.min(screenCap, Math.min(configured, wanted)); + } + private boolean canRenderReplacement() { + Minecraft mc = Minecraft.getInstance(); return mc != null && mc.player != null && mc.getConnection() != null; + } + private boolean shouldShowNow() { + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.player == null || mc.getConnection() == null || mc.options == null) return false; + if (!mc.options.keyPlayerList.isDown()) return false; + int listedPlayers = mc.getConnection().getListedOnlinePlayers().size(); + if (listedPlayers <= 0) return false; + if (!mc.isLocalServer() || listedPlayers > 1) return true; + Objective listObjective = null; + if (mc.level != null) { Scoreboard sb = mc.level.getScoreboard(); if (sb != null) listObjective = sb.getDisplayObjective(DisplaySlot.LIST); } + return listObjective != null; + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/TabListAnimator.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/TabListAnimator.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/TabListAnimator.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/TabListAnimator.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/TabListModel.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/TabListModel.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/TabListModel.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/TabListModel.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/TabRichTextRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/TabRichTextRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/TabRichTextRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/TabRichTextRenderer.java diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/data/TabListPalette.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/data/TabListPalette.java new file mode 100644 index 0000000..4190352 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/data/TabListPalette.java @@ -0,0 +1,61 @@ +package dev.loki.lovisual.features.gui.hud.nondraggable.impl.tab; + +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; + +import static dev.loki.lovisual.features.theme.Theme.theme; + +final class TabListPalette { + int fillTop; + int fillBottom; + int fillHotspot; + int stroke; + int strokeSoft; + int accent; + int accentSoft; + int accentGlow; + int text; + int muted; + int rowFill; + int rowAltFill; + int rowHighlight; + int pingChip; + int divider; + + void update(boolean syncTheme, int themeAlpha, int bgArgb, int accentArgb, int textArgb, int mutedArgb) { + if (syncTheme) { + int windowBg = HudRenderUtil.setAlpha(theme().windowBg(), themeAlpha); + fillTop = HudRenderUtil.mixRgb(windowBg, 0xFFFFFF, 0.060f); + fillBottom = HudRenderUtil.mixRgb(windowBg, 0x000000, 0.105f); + fillHotspot = HudRenderUtil.mixColor(fillTop, theme().accentSoft(), 0.12f); + stroke = theme().windowStroke(); + strokeSoft = theme().strokeSoft(); + accent = theme().accent(); + accentSoft = theme().accentSoft(); + accentGlow = HudRenderUtil.setAlpha(theme().accent(), 255); + text = theme().textPrimary(); + muted = theme().textMuted(); + rowFill = HudRenderUtil.setAlpha(theme().surface(), 122); + rowAltFill = HudRenderUtil.setAlpha(HudRenderUtil.mixRgb(theme().surface(), 0xFFFFFF, 0.026f), 100); + rowHighlight = HudRenderUtil.setAlpha(HudRenderUtil.mixRgb(theme().surface(), 0xFFFFFF, 0.070f), 130); + pingChip = HudRenderUtil.setAlpha(HudRenderUtil.mixRgb(theme().surface(), 0x000000, 0.05f), 128); + divider = HudRenderUtil.setAlpha(theme().strokeSoft(), 96); + return; + } + int baseBg = bgArgb; + fillTop = HudRenderUtil.mixRgb(baseBg, 0xFFFFFF, 0.060f); + fillBottom = HudRenderUtil.mixRgb(baseBg, 0x000000, 0.105f); + accent = accentArgb | 0xFF000000; + accentSoft = HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(accent, baseBg, 0.35f), 0.55f); + accentGlow = HudRenderUtil.setAlpha(accent, 255); + fillHotspot = HudRenderUtil.mixColor(fillTop, accentSoft, 0.12f); + text = textArgb | 0xFF000000; + muted = mutedArgb | 0xFF000000; + stroke = HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(accent, 0xFFFFFFFF, 0.18f), 0.62f); + strokeSoft = HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(muted, baseBg, 0.42f), 0.42f); + rowFill = HudRenderUtil.setAlpha(HudRenderUtil.mixRgb(baseBg, 0xFFFFFF, 0.045f), 122); + rowAltFill = HudRenderUtil.setAlpha(HudRenderUtil.mixRgb(baseBg, 0xFFFFFF, 0.024f), 100); + rowHighlight = HudRenderUtil.setAlpha(HudRenderUtil.mixRgb(baseBg, 0xFFFFFF, 0.075f), 130); + pingChip = HudRenderUtil.setAlpha(HudRenderUtil.mixRgb(baseBg, 0x000000, 0.06f), 128); + divider = HudRenderUtil.setAlpha(muted, 72); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/render/TabListRowRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/render/TabListRowRenderer.java new file mode 100644 index 0000000..1fe49ae --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/render/TabListRowRenderer.java @@ -0,0 +1,166 @@ +package dev.loki.lovisual.features.gui.hud.nondraggable.impl.tab; + +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; +import dev.loki.lovisual.features.relations.category.CategoryType; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; + +import java.util.ArrayList; +import java.util.List; + +final class TabListRowRenderer { + private static final float HEAD_SIZE = 30f; + private static final float ROW_PAD_X = 10.5f; + private static final float SHELL_PAD_X = 14f; + + private TabListRowRenderer() {} + + static List collect(float baseX, float baseY, float drawWidth, float panelAlpha, TabListModel.Snapshot snap, TabListAnimator animator, TabListPalette p) { + List entries = snap.entries(); + if (entries.isEmpty() || panelAlpha <= 0.01f) return List.of(); + int rowsPerColumn = Math.max(1, snap.rows()); + float contentX = contentX(baseX, drawWidth, snap); + float rowTop = snap.rowTop(); + float rowHeight = snap.rowHeight(); + float columnStride = snap.columnWidth() + snap.columnGap(); + List out = new ArrayList<>(entries.size()); + for (int i = 0; i < entries.size(); i++) { + TabListModel.Entry entry = entries.get(i); + TabListAnimator.RowState state = animator.row(entry.id()); + float presence = state.presence; + float rowAlpha = panelAlpha * AnimationUtility.easeOutCubic(presence); + if (rowAlpha <= 0.01f) continue; + int col = i / rowsPerColumn; + int row = i % rowsPerColumn; + float x = contentX + col * columnStride; + float y = baseY + rowTop + row * rowHeight + (1f - presence) * 10.0f; + float w = snap.columnWidth(); + float h = rowHeight - 5f; + int rowFill = (row & 1) == 0 ? p.rowFill : p.rowAltFill; + int relationColor = entry.relationColor() == 0 ? p.accent : entry.relationColor() | 0xFF000000; + boolean relation = entry.relation() != CategoryType.DEFAULT; + out.add(new RowRender(entry, x, y, w, h, rowAlpha, rowFill, relationColor, relation)); + } + return out; + } + + static void renderRows(Renderer2D renderer, List rows, TabListPalette p) { + if (rows.isEmpty()) return; + renderShadows(renderer, rows); + renderFills(renderer, rows, p); + renderRelationFills(renderer, rows); + renderRelationBars(renderer, rows); + renderStrokes(renderer, rows, p); + renderHeadPlates(renderer, rows, p); + renderHeadPlateStrokes(renderer, rows, p); + renderDividers(renderer, rows, p); + renderSpectatorBadges(renderer, rows, p); + renderScoreChipFills(renderer, rows, p); + renderScoreChipStrokes(renderer, rows, p); + renderPingChipFills(renderer, rows, p); + renderPingChipStrokes(renderer, rows, p); + renderPingChipGlows(renderer, rows); + renderPingChipOuterDots(renderer, rows); + renderPingChipInnerDots(renderer, rows); + renderPingChipDotStrokes(renderer, rows); + renderPingChipArcs(renderer, rows); + } + + private static void renderShadows(Renderer2D r, List rows) { + for (RowRender rr : rows) r.roundedRectSoftShadow(rr.x + 1.5f, rr.y + 2f, rr.w - 3f, rr.h, 9.5f, 6f, 0.18f, HudRenderUtil.scaleAlpha(0xFF000000, 0.14f * rr.alpha)); + } + private static void renderFills(Renderer2D r, List rows, TabListPalette p) { + for (RowRender rr : rows) r.roundedRectGradientQuad(rr.x, rr.y, rr.w, rr.h, 9.5f, HudRenderUtil.scaleAlpha(p.rowHighlight, rr.alpha), HudRenderUtil.scaleAlpha(rr.rowFill, rr.alpha), HudRenderUtil.scaleAlpha(rr.rowFill, rr.alpha), HudRenderUtil.scaleAlpha(p.rowHighlight, rr.alpha)); + } + private static void renderRelationFills(Renderer2D r, List rows) { + for (RowRender rr : rows) { + if (!rr.relation) continue; + int f = HudRenderUtil.scaleAlpha(rr.relationColor, 0.16f * rr.alpha); + r.roundedRectGradientQuad(rr.x + 1f, rr.y + 1f, rr.w - 2f, rr.h - 2f, 8.5f, f, HudRenderUtil.scaleAlpha(f, 0.42f), HudRenderUtil.scaleAlpha(f, 0.28f), HudRenderUtil.scaleAlpha(f, 0.68f)); + } + } + private static void renderRelationBars(Renderer2D r, List rows) { + for (RowRender rr : rows) if (rr.relation) r.roundedRectGradient(rr.x + 1.8f, rr.y + 6f, 2.25f, rr.h - 12f, 1.15f, HudRenderUtil.scaleAlpha(rr.relationColor, 0.62f * rr.alpha), HudRenderUtil.scaleAlpha(rr.relationColor, 0.18f * rr.alpha), 90f); + } + private static void renderStrokes(Renderer2D r, List rows, TabListPalette p) { + for (RowRender rr : rows) { + int strokeA = rr.relation ? HudRenderUtil.scaleAlpha(rr.relationColor, 0.52f * rr.alpha) : HudRenderUtil.scaleAlpha(p.stroke, 0.40f * rr.alpha); + r.roundedRectStrokeGradient(rr.x, rr.y, rr.w, rr.h, 9.5f, rr.relation ? 1.25f : 0.85f, strokeA, HudRenderUtil.scaleAlpha(p.strokeSoft, 0.24f * rr.alpha), 92f); + } + } + private static void renderHeadPlates(Renderer2D r, List rows, TabListPalette p) { + for (RowRender rr : rows) { + float px = rr.x + ROW_PAD_X - 3.5f; + float py = rr.y + (rr.h - HEAD_SIZE) * 0.5f - 3.5f; + r.roundedRectGradient(px, py, 37f, 37f, 7.75f, HudRenderUtil.scaleAlpha(p.rowHighlight, 0.72f * rr.alpha), HudRenderUtil.scaleAlpha(p.rowFill, 0.56f * rr.alpha), 92f); + } + } + private static void renderHeadPlateStrokes(Renderer2D r, List rows, TabListPalette p) { + for (RowRender rr : rows) { + float px = rr.x + ROW_PAD_X - 3.5f; + float py = rr.y + (rr.h - HEAD_SIZE) * 0.5f - 3.5f; + r.roundedRectStroke(px, py, 37f, 37f, 7.75f, 0f, 0.75f, HudRenderUtil.scaleAlpha(p.strokeSoft, 0.30f * rr.alpha)); + } + } + private static void renderDividers(Renderer2D r, List rows, TabListPalette p) { + for (RowRender rr : rows) r.roundedRectGradient(rr.x + ROW_PAD_X + HEAD_SIZE + 10.5f, rr.y + 8.5f, 1.15f, Math.max(6f, rr.h - 17f), 0.6f, HudRenderUtil.scaleAlpha(p.accentSoft, 0.20f * rr.alpha), HudRenderUtil.scaleAlpha(p.divider, rr.alpha), 90f); + } + private static void renderSpectatorBadges(Renderer2D r, List rows, TabListPalette p) { + for (RowRender rr : rows) if (rr.entry.spectator()) r.roundedRectGradient(rr.x + rr.w - 57f, rr.y + 8.5f, 19f, 4.5f, 2.25f, HudRenderUtil.scaleAlpha(p.muted, 0.40f * rr.alpha), HudRenderUtil.scaleAlpha(p.muted, 0.08f * rr.alpha), 0f); + } + private static void renderScoreChipFills(Renderer2D r, List rows, TabListPalette p) { + for (RowRender rr : rows) if (rr.entry.scoreText() != null) { ScoreChip c = scoreChip(rr); r.roundedRectGradient(c.x, c.y, c.w, c.h, 7.25f, HudRenderUtil.scaleAlpha(p.pingChip, 0.82f * rr.alpha), HudRenderUtil.scaleAlpha(p.pingChip, 0.50f * rr.alpha), 90f); } + } + private static void renderScoreChipStrokes(Renderer2D r, List rows, TabListPalette p) { + for (RowRender rr : rows) if (rr.entry.scoreText() != null) { ScoreChip c = scoreChip(rr); r.roundedRectStroke(c.x, c.y, c.w, c.h, 7.25f, 0f, 0.75f, HudRenderUtil.scaleAlpha(p.strokeSoft, 0.30f * rr.alpha)); } + } + private static void renderPingChipFills(Renderer2D r, List rows, TabListPalette p) { + for (RowRender rr : rows) { PingChip c = pingChip(rr); r.roundedRectGradient(c.x, c.y, c.w, c.h, 7.5f, HudRenderUtil.scaleAlpha(p.pingChip, 0.84f * rr.alpha), HudRenderUtil.scaleAlpha(p.pingChip, 0.52f * rr.alpha), 90f); } + } + private static void renderPingChipStrokes(Renderer2D r, List rows, TabListPalette p) { + for (RowRender rr : rows) { PingChip c = pingChip(rr); r.roundedRectStrokeGradient(c.x, c.y, c.w, c.h, 7.5f, 0.8f, HudRenderUtil.scaleAlpha(p.strokeSoft, 0.40f * rr.alpha), HudRenderUtil.scaleAlpha(p.strokeSoft, 0.18f * rr.alpha), 0f); } + } + private static void renderPingChipGlows(Renderer2D r, List rows) { + for (RowRender rr : rows) { PingChip c = pingChip(rr); r.circleSoftShadow(c.cx, c.cy, 5.4f, 7f, 0.34f, HudRenderUtil.scaleAlpha(pingBaseColor(rr.entry.latency()), 0.48f * rr.alpha)); } + } + private static void renderPingChipOuterDots(Renderer2D r, List rows) { + for (RowRender rr : rows) { PingChip c = pingChip(rr); r.circle(c.cx, c.cy, 4.4f, 0.95f, HudRenderUtil.scaleAlpha(pingBaseColor(rr.entry.latency()), 0.30f * rr.alpha)); } + } + private static void renderPingChipInnerDots(Renderer2D r, List rows) { + for (RowRender rr : rows) { PingChip c = pingChip(rr); r.circle(c.cx, c.cy, 3.35f, 0.9f, HudRenderUtil.scaleAlpha(pingBaseColor(rr.entry.latency()), rr.alpha)); } + } + private static void renderPingChipDotStrokes(Renderer2D r, List rows) { + for (RowRender rr : rows) { PingChip c = pingChip(rr); r.circleStroke(c.cx, c.cy, 4.15f, 0.7f, 0.8f, HudRenderUtil.scaleAlpha(0xFFFFFFFF, 0.36f * rr.alpha)); } + } + private static void renderPingChipArcs(Renderer2D r, List rows) { + for (RowRender rr : rows) { PingChip c = pingChip(rr); int base = pingBaseColor(rr.entry.latency()); r.arcStrokeGradient(c.cx, c.cy, 5.6f, 1.25f, -140f, 70f, 0.45f, HudRenderUtil.scaleAlpha(base, 0.68f * rr.alpha), HudRenderUtil.scaleAlpha(0xFFFFFFFF, 0.16f * rr.alpha), 35f); } + } + + static ScoreChip scoreChip(RowRender rr) { + float pingChipW = rr.entry.pingChipWidth(); + float occupiedLeft = rr.x + rr.w - 10.5f - pingChipW - 10f; + float scoreBoxW = rr.entry.scoreBoxWidth(); + float scoreBoxH = 23f; + return new ScoreChip(occupiedLeft - scoreBoxW - 8f, rr.y + (rr.h - scoreBoxH) * 0.5f, scoreBoxW, scoreBoxH); + } + static PingChip pingChip(RowRender rr) { + float chipW = rr.entry.pingChipWidth(); + float chipH = 22.5f; + float chipX = rr.x + rr.w - 10.5f - chipW; + float chipY = rr.y + (rr.h - chipH) * 0.5f; + return new PingChip(chipX, chipY, chipW, chipH, chipX + 10.2f, chipY + chipH * 0.5f); + } + static int pingBaseColor(int ping) { + if (ping < 0) return 0xFF8993A3; + if (ping < 90) return 0xFF52F2A1; + if (ping < 180) return 0xFFFFD166; + return 0xFFFF5A6E; + } + static float contentX(float baseX, float drawWidth, TabListModel.Snapshot snap) { + float contentWidth = snap.columns() * snap.columnWidth() + Math.max(0, snap.columns() - 1) * snap.columnGap(); + return baseX + Math.max(SHELL_PAD_X, (drawWidth - contentWidth) * 0.5f); + } + record RowRender(TabListModel.Entry entry, float x, float y, float w, float h, float alpha, int rowFill, int relationColor, boolean relation) {} + record ScoreChip(float x, float y, float w, float h) {} + record PingChip(float x, float y, float w, float h, float cx, float cy) {} +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/render/TabListShellRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/render/TabListShellRenderer.java new file mode 100644 index 0000000..961c25e --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/render/TabListShellRenderer.java @@ -0,0 +1,50 @@ +package dev.loki.lovisual.features.gui.hud.nondraggable.impl.tab; + +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; + +final class TabListShellRenderer { + private TabListShellRenderer() {} + + static void renderShell(Renderer2D renderer, float x, float y, float w, float h, float alpha, TabListPalette p) { + if (renderer == null || alpha <= 0.01f) return; + renderer.roundedRectShadow(x, y, w, h, 14.5f, 8f, 15f, HudRenderUtil.scaleAlpha(0xFF000000, 0.30f * alpha)); + renderer.roundedRectSoftShadow(x + 4f, y + 4f, Math.max(0f, w - 8f), Math.max(0f, h - 2f), 12.5f, 12f, 0.20f, HudRenderUtil.scaleAlpha(p.accentGlow, 0.16f * alpha)); + renderer.roundedRectGlow(x + 2f, y + 1.5f, Math.max(0f, w - 4f), Math.max(0f, h - 3f), 13f, 0f, 8f, HudRenderUtil.scaleAlpha(p.accentGlow, 0.14f * alpha)); + renderer.roundedRectGradientQuad(x, y, w, h, 14.5f, HudRenderUtil.scaleAlpha(p.fillHotspot, 0.96f * alpha), HudRenderUtil.scaleAlpha(p.fillTop, 0.96f * alpha), HudRenderUtil.scaleAlpha(p.fillBottom, 0.97f * alpha), HudRenderUtil.scaleAlpha(p.fillBottom, 0.97f * alpha)); + renderer.radialGlowMasked(x, y, w, h, 14.5f, 0f, Math.max(90f, w * 0.30f), x + w * 0.18f, y + 9f, HudRenderUtil.scaleAlpha(p.accentGlow, 0.26f * alpha)); + renderer.radialGlowMasked(x, y, w, h, 14.5f, 0f, Math.max(70f, w * 0.22f), x + w * 0.86f, y + h - 8f, HudRenderUtil.scaleAlpha(p.accentSoft, 0.16f * alpha)); + renderer.roundedRectStrokeGradient(x, y, w, h, 14.5f, 1.25f, HudRenderUtil.scaleAlpha(p.stroke, 0.82f * alpha), HudRenderUtil.scaleAlpha(p.strokeSoft, 0.48f * alpha), 115f); + float accentW = Math.max(62f, w * 0.34f); + renderer.roundedRectGradient(x + 21f, y + 11f, accentW, 1.45f, 0.8f, HudRenderUtil.scaleAlpha(p.accent, 0.58f * alpha), HudRenderUtil.scaleAlpha(p.accentSoft, 0.10f * alpha), 0f); + renderer.roundedRectGradient(x + w - 14f - accentW * 0.72f, y + h - 15.5f, accentW * 0.62f, 2.5f, 1.25f, HudRenderUtil.scaleAlpha(p.accentSoft, 0.06f * alpha), HudRenderUtil.scaleAlpha(p.accent, 0.25f * alpha), 0f); + } + + static void renderHeaderFooterBlocks(Renderer2D renderer, float baseX, float baseY, float width, float alpha, TabListModel.Snapshot snapshot, TabListPalette p) { + if (!snapshot.headerLines().isEmpty() && snapshot.headerHeight() > 0f) { + float x = baseX + 14f; + float y = baseY + snapshot.headerTop(); + float w = width - 28f; + float h = snapshot.headerHeight(); + renderer.roundedRectGradientQuad(x, y, w, h, 10.5f, HudRenderUtil.scaleAlpha(p.rowHighlight, 0.78f * alpha), HudRenderUtil.scaleAlpha(p.rowHighlight, 0.52f * alpha), HudRenderUtil.scaleAlpha(p.rowFill, 0.48f * alpha), HudRenderUtil.scaleAlpha(p.rowFill, 0.55f * alpha)); + renderer.roundedRectStrokeGradient(x, y, w, h, 10.5f, 0.95f, HudRenderUtil.scaleAlpha(p.stroke, 0.44f * alpha), HudRenderUtil.scaleAlpha(p.strokeSoft, 0.18f * alpha), 96f); + renderer.roundedRectGradient(x + 12f, y + h - 5.5f, Math.max(30f, w * 0.20f), 1.25f, 0.65f, HudRenderUtil.scaleAlpha(p.accent, 0.44f * alpha), HudRenderUtil.scaleAlpha(p.accentSoft, 0.06f * alpha), 0f); + } + if (!snapshot.footerLines().isEmpty() && snapshot.footerHeight() > 0f) { + float x = baseX + 14f; + float y = baseY + snapshot.footerTop(); + float w = width - 28f; + float h = snapshot.footerHeight(); + renderer.roundedRectGradientQuad(x, y, w, h, 10f, HudRenderUtil.scaleAlpha(p.rowFill, 0.34f * alpha), HudRenderUtil.scaleAlpha(p.rowFill, 0.46f * alpha), HudRenderUtil.scaleAlpha(p.rowFill, 0.46f * alpha), HudRenderUtil.scaleAlpha(p.rowFill, 0.34f * alpha)); + renderer.roundedRectStroke(x, y, w, h, 10f, 0f, 0.8f, HudRenderUtil.scaleAlpha(p.strokeSoft, 0.30f * alpha)); + float accentW = Math.max(42f, w * 0.24f); + renderer.roundedRectGradient(x + w - accentW - 12f, y + h - 5.5f, accentW, 1.15f, 0.6f, HudRenderUtil.scaleAlpha(p.accentSoft, 0.04f * alpha), HudRenderUtil.scaleAlpha(p.accent, 0.32f * alpha), 0f); + } + } + + static void renderChrome(Renderer2D renderer, float drawX, float drawY, float width, float height, float alpha, TabListModel.Snapshot snapshot, TabListPalette p) { + if (renderer == null || alpha <= 0.01f) return; + renderShell(renderer, drawX, drawY, width, height, alpha, p); + renderHeaderFooterBlocks(renderer, drawX, drawY, width, alpha, snapshot, p); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/render/TabListTextRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/render/TabListTextRenderer.java new file mode 100644 index 0000000..faa1841 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/render/TabListTextRenderer.java @@ -0,0 +1,157 @@ +package dev.loki.lovisual.features.gui.hud.nondraggable.impl.tab; + +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.color.RenderColor; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.helpers.player.PlayerHeadRenderer; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.network.chat.Component; + +final class TabListTextRenderer { + private static final float HEAD_SIZE = 30f; + private static final float ROW_PAD_X = 10.5f; + private static final float SHELL_PAD_X = 14f; + private static final float TEXT_SIZE = 24.5f; + private static final float PING_TEXT_SIZE = 17.5f; + private static final float SCORE_TEXT_SIZE = 20.5f; + private static final float HEADER_TEXT_SIZE = 24f; + private static final float FOOTER_TEXT_SIZE = 20.5f; + + private TabListTextRenderer() {} + + static void renderHeadsAndText(GuiGraphicsExtractor ctx, Renderer2D renderer, float baseX, float baseY, float drawWidth, float panelAlpha, TabListModel.Snapshot snap, TabListAnimator animator, TabListPalette p) { + float textH = TabRichTextRenderer.height(TEXT_SIZE); + renderHeaderText(renderer, baseX, baseY, drawWidth, panelAlpha, snap); + renderHeads(ctx, baseX, baseY, drawWidth, panelAlpha, snap, animator); + renderPingTexts(renderer, baseX, baseY, drawWidth, panelAlpha, snap, animator, p); + renderScoreTexts(renderer, baseX, baseY, drawWidth, panelAlpha, snap, animator, p); + renderNameTexts(renderer, baseX, baseY, drawWidth, panelAlpha, snap, animator, p, textH); + renderFooterText(renderer, baseX, baseY, drawWidth, panelAlpha, snap); + } + + private static void renderHeads(GuiGraphicsExtractor ctx, float baseX, float baseY, float drawWidth, float panelAlpha, TabListModel.Snapshot snap, TabListAnimator animator) { + float contentX = contentX(baseX, drawWidth, snap); + float rowTop = snap.rowTop(); + for (int i = 0; i < snap.entries().size(); i++) { + TabListModel.Entry entry = snap.entries().get(i); + TabListAnimator.RowState state = animator.row(entry.id()); + float rowAlpha = panelAlpha * AnimationUtility.easeOutCubic(state.presence); + if (rowAlpha <= 0.01f) continue; + int col = i / Math.max(1, snap.rows()); + int row = i % Math.max(1, snap.rows()); + float rowX = contentX + col * (snap.columnWidth() + snap.columnGap()); + float rowY = baseY + rowTop + row * snap.rowHeight() + (1f - state.presence) * 10.0f; + float rowH = snap.rowHeight() - 5f; + float headX = rowX + ROW_PAD_X; + float headY = rowY + (rowH - HEAD_SIZE) * 0.5f; + int alpha = Math.max(0, Math.min(255, Math.round(255f * rowAlpha))); + if (entry.skin() != null) { + PlayerHeadRenderer.drawRounded(ctx, headX, headY, HEAD_SIZE, 5.25f, entry.skin(), new RenderColor(255, 255, 255, alpha), true, new RenderColor(255, 255, 255, Math.round(42f * rowAlpha)), 1.2f, false); + } + } + } + + private static void renderPingTexts(Renderer2D renderer, float baseX, float baseY, float drawWidth, float panelAlpha, TabListModel.Snapshot snap, TabListAnimator animator, TabListPalette p) { + float contentX = contentX(baseX, drawWidth, snap); + float pingTextH = TabRichTextRenderer.height(PING_TEXT_SIZE); + float rowTop = snap.rowTop(); + for (int i = 0; i < snap.entries().size(); i++) { + TabListModel.Entry entry = snap.entries().get(i); + TabListAnimator.RowState state = animator.row(entry.id()); + float rowAlpha = panelAlpha * AnimationUtility.easeOutCubic(state.presence); + if (rowAlpha <= 0.01f) continue; + int col = i / Math.max(1, snap.rows()); + int row = i % Math.max(1, snap.rows()); + float rowX = contentX + col * (snap.columnWidth() + snap.columnGap()); + float rowY = baseY + rowTop + row * snap.rowHeight() + (1f - state.presence) * 10.0f; + float rowH = snap.rowHeight() - 5f; + float right = rowX + snap.columnWidth() - 10.5f; + float pingChipW = entry.pingChipWidth(); + float pingChipH = 22.5f; + float pingChipX = right - pingChipW; + float pingChipY = rowY + (rowH - pingChipH) * 0.5f; + TabRichTextRenderer.drawPlain(renderer, entry.pingText(), pingChipX + 19.5f, pingChipY + (pingChipH - pingTextH) * 0.5f - 0.25f, pingChipW - 22f, PING_TEXT_SIZE, p.text, rowAlpha); + } + } + + private static void renderScoreTexts(Renderer2D renderer, float baseX, float baseY, float drawWidth, float panelAlpha, TabListModel.Snapshot snap, TabListAnimator animator, TabListPalette p) { + float contentX = contentX(baseX, drawWidth, snap); + float scoreTextH = TabRichTextRenderer.height(SCORE_TEXT_SIZE); + float rowTop = snap.rowTop(); + for (int i = 0; i < snap.entries().size(); i++) { + TabListModel.Entry entry = snap.entries().get(i); + if (entry.scoreText() == null) continue; + TabListAnimator.RowState state = animator.row(entry.id()); + float rowAlpha = panelAlpha * AnimationUtility.easeOutCubic(state.presence); + if (rowAlpha <= 0.01f) continue; + int col = i / Math.max(1, snap.rows()); + int row = i % Math.max(1, snap.rows()); + float rowX = contentX + col * (snap.columnWidth() + snap.columnGap()); + float rowY = baseY + rowTop + row * snap.rowHeight() + (1f - state.presence) * 10.0f; + float rowH = snap.rowHeight() - 5f; + float right = rowX + snap.columnWidth() - 10.5f; + float occupiedLeft = right - entry.pingChipWidth() - 10f; + float scoreBoxW = entry.scoreBoxWidth(); + float scoreBoxH = 23f; + float scoreX = occupiedLeft - scoreBoxW - 8f; + float scoreY = rowY + (rowH - scoreBoxH) * 0.5f; + TabRichTextRenderer.draw(renderer, entry.scoreText(), scoreX + 8f, scoreY + (scoreBoxH - scoreTextH) * 0.5f - 0.5f, entry.scoreWidth() + 2f, SCORE_TEXT_SIZE, p.text, rowAlpha); + } + } + + private static void renderNameTexts(Renderer2D renderer, float baseX, float baseY, float drawWidth, float panelAlpha, TabListModel.Snapshot snap, TabListAnimator animator, TabListPalette p, float textH) { + float contentX = contentX(baseX, drawWidth, snap); + float rowTop = snap.rowTop(); + for (int i = 0; i < snap.entries().size(); i++) { + TabListModel.Entry entry = snap.entries().get(i); + TabListAnimator.RowState state = animator.row(entry.id()); + float rowAlpha = panelAlpha * AnimationUtility.easeOutCubic(state.presence); + if (rowAlpha <= 0.01f) continue; + int col = i / Math.max(1, snap.rows()); + int row = i % Math.max(1, snap.rows()); + float rowX = contentX + col * (snap.columnWidth() + snap.columnGap()); + float rowY = baseY + rowTop + row * snap.rowHeight() + (1f - state.presence) * 10.0f; + float rowH = snap.rowHeight() - 5f; + float right = rowX + snap.columnWidth() - 10.5f; + float pingChipX = right - entry.pingChipWidth(); + float occupiedLeft = pingChipX - 10f; + if (entry.scoreText() != null) occupiedLeft = occupiedLeft - entry.scoreBoxWidth() - 18f; + float textX = rowX + ROW_PAD_X + HEAD_SIZE + 22f; + float textY = rowY + (rowH - textH) * 0.5f - 1.0f; + float textW = Math.max(40f, occupiedLeft - textX); + int baseColor = entry.spectator() ? p.muted : p.text; + TabRichTextRenderer.draw(renderer, entry.displayName(), textX, textY, textW, TEXT_SIZE, baseColor, rowAlpha); + } + } + + private static void renderHeaderText(Renderer2D renderer, float baseX, float baseY, float drawWidth, float alpha, TabListModel.Snapshot snap) { + if (snap.headerLines().isEmpty() || alpha <= 0.01f) return; + float lineH = 29f; + float y = baseY + snap.headerTop() + 8f + (lineH - TabRichTextRenderer.height(HEADER_TEXT_SIZE)) * 0.5f; + for (Component line : snap.headerLines()) { + drawCentered(renderer, line, baseX + drawWidth * 0.5f, y, drawWidth - 56f, HEADER_TEXT_SIZE, 0xFFFFFFFF, alpha); + y += lineH; + } + } + + private static void renderFooterText(Renderer2D renderer, float baseX, float baseY, float drawWidth, float alpha, TabListModel.Snapshot snap) { + if (snap.footerLines().isEmpty() || alpha <= 0.01f) return; + float lineH = 25f; + float y = baseY + snap.footerTop() + 7f + (lineH - TabRichTextRenderer.height(FOOTER_TEXT_SIZE)) * 0.5f; + for (Component line : snap.footerLines()) { + drawCentered(renderer, line, baseX + drawWidth * 0.5f, y, drawWidth - 56f, FOOTER_TEXT_SIZE, 0xFFFFFFFF, alpha); + y += lineH; + } + } + + private static void drawCentered(Renderer2D renderer, Component text, float centerX, float y, float maxWidth, float size, int color, float alpha) { + if (text == null || alpha <= 0.01f || maxWidth <= 0f) return; + float width = Math.min(maxWidth, TabRichTextRenderer.width(text, size)); + TabRichTextRenderer.draw(renderer, text, centerX - width * 0.5f, y, maxWidth, size, color, alpha); + } + + private static float contentX(float baseX, float drawWidth, TabListModel.Snapshot snap) { + float contentWidth = snap.columns() * snap.columnWidth() + Math.max(0, snap.columns() - 1) * snap.columnGap(); + return baseX + Math.max(SHELL_PAD_X, (drawWidth - contentWidth) * 0.5f); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tooltips/BetterTooltips.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tooltips/BetterTooltips.java new file mode 100644 index 0000000..082eb44 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tooltips/BetterTooltips.java @@ -0,0 +1,181 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.nondraggable.impl.tooltips; + +import dev.loki.lovisual.config.definition.SettingDef; +import dev.loki.lovisual.config.values.collection.ConfigValue; +import dev.loki.lovisual.config.values.collection.ItemIdSetValue; +import dev.loki.lovisual.config.values.color.KeyBindValue; +import dev.loki.lovisual.config.values.mode.BindMode; +import dev.loki.lovisual.config.values.mode.ModeValue; +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import dev.loki.lovisual.features.gui.hud.base.AbstractHudElement; +import dev.loki.lovisual.features.gui.hud.registry.HudElementRegister; +import dev.loki.lovisual.features.gui.hud.script.HudScriptLayouts; +import dev.loki.lovisual.features.theme.Theme; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.runtime.CachedUiScriptRuntime; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.module.UiScriptModuleHandle; +import dev.loki.lovisual.util.input.KeyManager; +import dev.loki.lovisual.util.item.enchant.TopEnchantUtil; +import dev.loki.lovisual.features.gui.hud.nondraggable.impl.buttons.BetterButtons; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipPositioner; +import net.minecraft.util.FormattedCharSequence; +import net.minecraft.world.item.ItemStack; +import java.util.ArrayList; +import java.util.List; + +@HudElementRegister(order = 20) +public final class BetterTooltips extends AbstractHudElement { + + private static final String[] TOOLTIP_FONT_OPTIONS = { + "Onest", "OnestMedium", "OnestLight", "Inter", "InterMedium", "Comfortaa", "Iosevka", "ProFont", "Monsterrat" + }; + public static final BetterTooltips INSTANCE = new BetterTooltips(); + public static final String GROUP_TOOLTIP_GUI = "vanilla_tooltip_gui"; + public static final String GROUP_TOOLTIP_ITEMS = "vanilla_tooltip_items"; + public static final String GROUP_SHULKER_PREVIEW = "vanilla_shulker_preview"; + private static final String TOOLTIP_PANEL_LAYOUT_ID = "lovisual:api/hud/static/tooltip_panel"; + static final int TOOLTIP_PAD_X = 4; + static final int TOOLTIP_PAD_Y = 3; + static final float TOOLTIP_STROKE_WIDTH = 0.55f; + static final float TOOLTIP_GLOW_SIZE = 3.75f; + static final int TOOLTIP_BG_ALPHA = 0xEE; + static final int TOOLTIP_STROKE_ALPHA = 0xDC; + static final int TOOLTIP_GLOW_ALPHA = 0x5C; + static final float TOOLTIP_TEXT_SCALE = 0.35f; + static final int TOOLTIP_LINE_GAP = 0; + static final int TOOLTIP_PREVIEW_GAP = 3; + static final int TOOLTIP_TITLE_GAP = 2; + static final float SHULKER_BG_RADIUS = 1.5f; + static final float SHULKER_BG_SOFTNESS = 0.45f; + static final int SHULKER_SLOT_BG = 0xFF1A1C20; + static final int SHULKER_SLOT_BORDER = 0x8A34383E; + static final float SHULKER_SLOT_RADIUS = 2.0f; + static final float SHULKER_SLOT_SOFTNESS = 0.65f; + static final float SHULKER_SLOT_GAP = 2.0f; + static final float SHULKER_PADDING = 4.0f; + + private final BooleanValue itemTooltipEnabled = bool("item_tooltip", true); + private final BooleanValue shulkerPreviewEnabled = bool("shulker_preview", true); + private final BooleanValue itemInfoColorize = bool("item_info_colorize", true); + private final ModeValue tooltipFont = mode("tooltip_font", "Iosevka", TOOLTIP_FONT_OPTIONS); + private final NumberValue tooltipTextSize = num("tooltip_text_size", 1.0f, 0.5f, 2.0f); + private final ItemIdSetValue topIgnore = registerTopIgnore(); + private final NumberValue shulkerPreviewSlotSize = + visibleWhen(num("shulker_slot_size", 18.0f, 12.0f, 24.0f), this::isShulkerPreviewEnabled); + private final KeyBindValue shulkerPreviewHold = + visibleWhen(bind("shulker_preview_hold", "LEFT_SHIFT", BindMode.HOLD), this::isShulkerPreviewEnabled); + private final UiScriptModuleHandle tooltipPanelHandle = HudScriptLayouts.handle(TOOLTIP_PANEL_LAYOUT_ID); + private final CachedUiScriptRuntime tooltipRuntime = new CachedUiScriptRuntime(HudScriptLayouts.runtimeReporter()); + + private BetterTooltips() { + super("vanilla_tooltips", "Tooltips", true); + } + + public static BetterTooltips get() { return INSTANCE; } + public static boolean isGroupId(String id) { return SettingsGroup.fromId(id) != null; } + public static void beginTooltipFrame() { TooltipState.beginFrame(); } + public static boolean hasTooltip() { return TooltipState.hasTooltip(); } + public static void captureTooltipOrdered(List l, ClientTooltipPositioner p, int x, int y) { + TooltipState.captureOrdered(l, p, x, y); + } + public static void captureItemTooltipOrdered(List l, ClientTooltipPositioner p, int x, int y, ItemStack s, GuiGraphicsExtractor c) { + TooltipState.captureItemOrdered(l, p, x, y, s, c); + } + public static void setDrawContext(GuiGraphicsExtractor c) { TooltipState.setDrawContext(c); } + public static void setLastTooltipStack(ItemStack s) { TooltipState.setLastStack(s); } + public static ItemStack consumeLastTooltipStack() { return TooltipState.consumeLastStack(); } + public static void renderTooltip() { + if (!hasTooltip()) return; + renderTooltipInternal(TooltipState.getLines(), TooltipState.getPositioner(), TooltipState.getMouseX(), TooltipState.getMouseY()); + } + public static void setTooltipScaleOverride(float s) { TooltipState.setScaleOverride(s); } + public static void resetTooltipScaleOverride() { TooltipState.resetScaleOverride(); } + public static void renderTooltipWithContext(GuiGraphicsExtractor c) { + if (c != null && TooltipState.getCtx() == null) TooltipState.setDrawContext(c); + renderTooltip(); + } + + private static void renderTooltipInternal(List lines, ClientTooltipPositioner pos, int mouseX, int mouseY) { + TooltipPanelRenderer.renderTooltipInternal(INSTANCE, lines, pos, mouseX, mouseY); + } + String tooltipFontValue() { return tooltipFont.get(); } + float tooltipTextSizeValue() { return tooltipTextSize.get(); } + UiScriptModuleHandle tooltipPanelHandle() { return tooltipPanelHandle; } + CachedUiScriptRuntime tooltipRuntime() { return tooltipRuntime; } + + @Override protected void defineSettings(List defs) { defs.add(0, enabledSettingDef()); } + @Override protected void onLoaded() { registerBind(shulkerPreviewHold); } + + public List getSettingsGroup(String groupId) { + SettingsGroup g = SettingsGroup.fromId(groupId); + if (g == null) return List.of(); + List defs = new ArrayList<>(); + switch (g) { + case TOOLTIP_GUI -> defs.add(enabledSettingDef()); + case TOOLTIP_ITEMS -> add(defs, itemTooltipEnabled, itemInfoColorize, topIgnore); + case SHULKER_PREVIEW -> add(defs, shulkerPreviewEnabled, shulkerPreviewSlotSize, shulkerPreviewHold); + } + return defs; + } + + private ItemIdSetValue registerTopIgnore() { + ItemIdSetValue v = TopEnchantUtil.ignoreValue(); + return declareSetting(v, SettingDef.textList(v)); + } + + private void add(List defs, ConfigValue... vs) { + for (ConfigValue v : vs) { SettingDef d = settingDef(v); if (d != null) defs.add(d); } + } + + public boolean useCustomGuiTooltips() { BetterButtons b = BetterButtons.get(); if (b != null && b.useUiButtons()) return true; return isEnabled(); } + public boolean useCustomItemTooltips() { BetterButtons b = BetterButtons.get(); if (b != null && b.useUiButtons()) return true; return itemTooltipEnabled.get(); } + public boolean isItemInfoColorizeEnabled() { return itemInfoColorize.get(); } + public boolean isTooltipAlphaEnabled() { return anyTooltipsEnabled(); } + public int tooltipColor() { return (TOOLTIP_BG_ALPHA << 24) | 0x00101114; } + public boolean isShulkerPreviewEnabled() { return shulkerPreviewEnabled.get(); } + public void setShulkerPreviewEnabled(boolean v) { shulkerPreviewEnabled.set(v); saveConfig(); } + public boolean isGuiTooltipEnabled() { return isEnabled(); } + public void setGuiTooltipEnabled(boolean v) { setEnabled(v); } + public boolean isItemTooltipEnabled() { return itemTooltipEnabled.get(); } + public void setItemTooltipEnabled(boolean v) { itemTooltipEnabled.set(v); saveConfig(); } + public boolean isShulkerPreviewHeld() { return KeyManager.isHeldAllowScreen(bindingName(shulkerPreviewHold)); } + public String getShulkerPreviewBind() { return shulkerPreviewHold.get(); } + public int shulkerPreviewBgRgb() { return Theme.theme().windowBg() & 0x00FFFFFF; } + public int shulkerPreviewBgAlpha() { return TOOLTIP_BG_ALPHA; } + public float shulkerPreviewBgRadius() { return SHULKER_BG_RADIUS; } + public float shulkerPreviewBgSoftness() { return SHULKER_BG_SOFTNESS; } + public int shulkerPreviewSlotBgRgb() { return SHULKER_SLOT_BG & 0x00FFFFFF; } + public int shulkerPreviewSlotBorderRgb() { return SHULKER_SLOT_BORDER & 0x00FFFFFF; } + public int shulkerPreviewSlotBorderAlpha() { return (SHULKER_SLOT_BORDER >>> 24) & 0xFF; } + public float shulkerPreviewSlotRadius() { return SHULKER_SLOT_RADIUS; } + public float shulkerPreviewSlotSoftness() { return SHULKER_SLOT_SOFTNESS; } + public float shulkerPreviewSlotSize() { return shulkerPreviewSlotSize.get(); } + public float shulkerPreviewSlotGap() { return SHULKER_SLOT_GAP; } + public float shulkerPreviewPadding() { return SHULKER_PADDING; } + + private boolean anyTooltipsEnabled() { if (useCustomGuiTooltips()) return true; if (itemTooltipEnabled.get()) return true; return shulkerPreviewEnabled.get(); } + private String bindingName(KeyBindValue v) { return name() + ":" + v.getName(); } + private void registerBind(KeyBindValue v) { + String n = bindingName(v); KeyManager.unregisterAll(n); + if (!v.isNone()) KeyManager.registerCombo(n, v.get()); + } + + private enum SettingsGroup { + TOOLTIP_GUI(GROUP_TOOLTIP_GUI), TOOLTIP_ITEMS(GROUP_TOOLTIP_ITEMS), SHULKER_PREVIEW(GROUP_SHULKER_PREVIEW); + private final String id; + SettingsGroup(String id) { this.id = id; } + static SettingsGroup fromId(String id) { + if (id == null) return null; + for (SettingsGroup g : values()) if (g.id.equals(id)) return g; + return null; + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tooltips/render/ShulkerPreviewRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tooltips/render/ShulkerPreviewRenderer.java new file mode 100644 index 0000000..eee0494 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tooltips/render/ShulkerPreviewRenderer.java @@ -0,0 +1,79 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.nondraggable.impl.tooltips; + +import dev.loki.lovisual.render.engine.core.context.ViewportContext; +import dev.loki.lovisual.render.engine.profiler.config.ProfilerPhase; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.world.item.ItemStack; + +final class ShulkerPreviewRenderer { + + private ShulkerPreviewRenderer() { + } + + static void render(PreviewLayout preview, int baseX, int baseY, GuiGraphicsExtractor ctx) { + if (preview == null) return; + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.getWindow() == null) return; + int slotBg = BetterTooltips.SHULKER_SLOT_BG; + int border = BetterTooltips.SHULKER_SLOT_BORDER; + float scale = ViewportContext.getScaleFactor(); + ViewportContext.beginUnscaled(ctx); + Renderer2D.COLOR.begin(); + float stroke = 0.35f * scale; + for (int row = 0; row < preview.rows(); row++) { + for (int col = 0; col < preview.cols(); col++) { + float sx = baseX + preview.pad() + col * (preview.slotSize() + preview.slotGap()); + float sy = baseY + preview.pad() + row * (preview.slotSize() + preview.slotGap()); + float xb = sx * scale; + float yb = sy * scale; + float wb = preview.slotSize() * scale; + float hb = preview.slotSize() * scale; + Renderer2D.COLOR.roundedRect(xb, yb, wb, hb, + BetterTooltips.SHULKER_SLOT_RADIUS * scale, + BetterTooltips.SHULKER_SLOT_SOFTNESS * scale, slotBg); + Renderer2D.COLOR.roundedRectStroke(xb, yb, wb, hb, + BetterTooltips.SHULKER_SLOT_RADIUS * scale, + BetterTooltips.SHULKER_SLOT_SOFTNESS * scale, stroke, border); + } + } + Renderer2D.COLOR.render(); + ViewportContext.end(ctx); + if (ctx == null) return; + ViewportContext.beginUnscaled(ctx); + Renderer2D.COLOR.begin(); + float offset = (preview.slotSize() - 16f) * 0.5f; + try (ProfilerPhase.Scope ignored = ProfilerPhase.scope("2d:tooltip:shulker_preview")) { + int seed = 1; + int idx = 0; + for (int row = 0; row < preview.rows(); row++) { + for (int col = 0; col < preview.cols(); col++) { + if (idx >= preview.stacks().size()) { + Renderer2D.COLOR.render(); + ViewportContext.end(ctx); + return; + } + ItemStack stack = preview.stacks().get(idx); + if (stack == null || stack.isEmpty()) { + idx++; + continue; + } + float sx = (baseX + preview.pad() + col * (preview.slotSize() + preview.slotGap()) + offset) * scale; + float sy = (baseY + preview.pad() + row * (preview.slotSize() + preview.slotGap()) + offset) * scale; + Renderer2D.COLOR.item(stack, sx, sy, scale, seed++, Renderer2D.ITEM_OVERLAY_ALL, null); + idx++; + } + } + Renderer2D.COLOR.render(); + ViewportContext.end(ctx); + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tooltips/render/TooltipPanelRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tooltips/render/TooltipPanelRenderer.java new file mode 100644 index 0000000..4254802 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tooltips/render/TooltipPanelRenderer.java @@ -0,0 +1,184 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.nondraggable.impl.tooltips; + +import dev.loki.lovisual.features.gui.hud.script.HudScriptLayouts; +import dev.loki.lovisual.features.theme.Theme; +import dev.loki.lovisual.features.theme.Themes; +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; +import dev.loki.lovisual.render.engine.color.RenderColor; +import dev.loki.lovisual.render.engine.core.context.ViewportContext; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.runtime.UiRuntime; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.render.context.UiProjectionMode; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.render.context.UiRenderContext; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.module.UiScriptModule; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.module.UiScriptModuleHandle; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.script.runtime.CachedUiScriptRuntime; +import dev.loki.lovisual.render.engine.text.font.Fonts; +import dev.loki.lovisual.render.engine.text.font.info.FontInfo; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipPositioner; +import org.joml.Vector2ic; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +final class TooltipPanelRenderer { + + private TooltipPanelRenderer() { + } + + static String hex(int argb) { + return String.format("#%08X", argb); + } + + static void drawFallback(float x, float y, float width, float height, + int bg, int tl, int tr, int br, int bl, int glow) { + float scale = ViewportContext.getScaleFactor(); + float xb = x * scale; + float yb = y * scale; + float wb = width * scale; + float hb = height * scale; + float stroke = BetterTooltips.TOOLTIP_STROKE_WIDTH * scale; + ViewportContext.unscaledProjection(); + Renderer2D.COLOR.roundedRectGlow(xb, yb, wb, hb, 0.0f, 0.0f, + BetterTooltips.TOOLTIP_GLOW_SIZE * scale, glow); + Renderer2D.COLOR.quad(xb, yb, wb, hb, bg); + drawStroke(Renderer2D.COLOR, xb, yb, wb, hb, stroke, tl, tr, br, bl); + ViewportContext.scaledProjection(); + } + + static void drawStroke(Renderer2D r, float x, float y, float w, float h, float t, + int ctl, int ctr, int cbr, int cbl) { + if (r == null || t <= 0 || w <= 0 || h <= 0) return; + float th = Math.min(t, Math.min(w, h) * 0.5f); + r.quadGradient(x, y, w, th, ctl, ctr, ctr, ctl); + r.quadGradient(x, y + h - th, w, th, cbl, cbr, cbr, cbl); + float inner = Math.max(0, h - th * 2); + if (inner > 0) { + r.quadGradient(x, y + th, th, inner, ctl, ctl, cbl, cbl); + r.quadGradient(x + w - th, y + th, th, inner, ctr, ctr, cbr, cbr); + } + } + + static boolean renderScripted(Minecraft mc, Renderer2D renderer, TextRenderer tr, + float x, float y, float w, float h, + int bg, int tl, int trc, int br, int bl, int glow, + float strokeW, float glowSize, float alpha, + UiScriptModuleHandle handle, CachedUiScriptRuntime runtime, + GuiGraphicsExtractor ctx) { + if (mc == null || renderer == null || tr == null || handle.isRuntimeBlocked()) return false; + HudScriptLayouts.pollReloadCombo(mc); + if (handle.consumeChanged()) runtime.reset(); + UiScriptModule module = ensureModule(mc, handle); + if (module == null) return false; + float fb = ViewportContext.getScaleFactor(); + float dx = x * fb; + float dy = y * fb; + float dw = w * fb; + float dh = h * fb; + float sw = strokeW * fb; + float gs = glowSize * fb; + Map props = props(dw, dh, bg, tl, trc, br, bl, glow, sw, gs); + long sig = CachedUiScriptRuntime.signature(props); + long layoutSig = CachedUiScriptRuntime.mix(CachedUiScriptRuntime.mix(sig, dx), dy); + UiRuntime baked = runtime.bake(handle, module, "tooltip_panel", sig, layoutSig, + dw, dh, tr, dx, dy, dw, dh, () -> props); + if (baked == null) return false; + ViewportContext.unscaledProjection(); + boolean started = !Renderer2D.isBatching(); + if (started) renderer.begin(); + baked.render(new UiRenderContext(renderer, tr, ctx, 0.0f, UiProjectionMode.RAW_FRAMEBUFFER, alpha)); + if (started) renderer.render(); + ViewportContext.scaledProjection(); + return true; + } + + static Map props(float dw, float dh, int bg, int tl, int tr, int br, int bl, int glow, float sw, float gs) { + LinkedHashMap p = new LinkedHashMap<>(12); + p.put("width", dw); + p.put("height", dh); + p.put("bg", hex(bg)); + p.put("strokeTopLeft", hex(tl)); + p.put("strokeTopRight", hex(tr)); + p.put("strokeBottomRight", hex(br)); + p.put("strokeBottomLeft", hex(bl)); + p.put("strokeWidth", sw); + p.put("glow", hex(glow)); + p.put("glowSize", gs); + return p; + } + + private static UiScriptModule ensureModule(Minecraft mc, UiScriptModuleHandle handle) { + if (mc == null || mc.getResourceManager() == null) return null; + if (!handle.ensureLoaded(mc.getResourceManager())) { + HudScriptLayouts.reportLoadError(handle); + return null; + } + handle.consumeChanged(); + return handle.module(); + } + + static void renderTooltipInternal(BetterTooltips instance, List lines, ClientTooltipPositioner pos, int mouseX, int mouseY) { + Minecraft mc = Minecraft.getInstance(); + if (mc == null) return; + TextRenderer tr = Fonts.renderer(instance.tooltipFontValue(), FontInfo.Type.Regular, TextRenderer.get()); + if (tr == null) return; + PreviewLayout preview = ShulkerPreviewCache.buildLayout(); + List finalLines = TooltipState.prepareLines(lines, preview != null); + float scale = BetterTooltips.TOOLTIP_TEXT_SCALE * TooltipState.getScaleOverride() * instance.tooltipTextSizeValue(); + tr.begin(scale, false, false); + int maxW = 0; + for (TooltipLine l : finalLines) maxW = Math.max(maxW, (int) Math.ceil(tr.getWidth(l.text(), false))); + int lh = (int) Math.ceil(tr.getHeight(false)); + tr.end(); + if (finalLines.isEmpty()) finalLines = List.of(new TooltipLine(" ", Theme.theme().textPrimary())); + int textH = finalLines.size() * lh + (finalLines.size() > 1 ? BetterTooltips.TOOLTIP_TITLE_GAP : 0) + Math.max(0, finalLines.size() - 2) * BetterTooltips.TOOLTIP_LINE_GAP; + int pw = preview != null ? preview.width() : 0; + int ph = preview != null ? preview.height() : 0; + int contentW = Math.max(maxW, pw); + int totalW = contentW + BetterTooltips.TOOLTIP_PAD_X * 2; + int totalH = BetterTooltips.TOOLTIP_PAD_Y * 2 + textH + (preview != null ? BetterTooltips.TOOLTIP_PREVIEW_GAP + ph : 0); + int x = mouseX, y = mouseY; + if (pos != null) { + Vector2ic p = pos.positionTooltip(mc.getWindow().getGuiScaledWidth(), mc.getWindow().getGuiScaledHeight(), mouseX, mouseY, totalW, totalH); + x = p.x(); y = p.y(); + } + Themes.Theme theme = Theme.theme(); + int bg = HudRenderUtil.setAlpha(theme.windowBg(), BetterTooltips.TOOLTIP_BG_ALPHA); + int edge = HudRenderUtil.mixColor(theme.accentSoft(), theme.accent(), 0.34f); + int glow = HudRenderUtil.setAlpha(edge, BetterTooltips.TOOLTIP_GLOW_ALPHA); + int tl = HudRenderUtil.setAlpha(HudRenderUtil.mixColor(theme.accentSoft(), theme.accent(), 0.14f), BetterTooltips.TOOLTIP_STROKE_ALPHA); + int trc = HudRenderUtil.setAlpha(HudRenderUtil.mixColor(theme.accentSoft(), theme.accent(), 0.82f), BetterTooltips.TOOLTIP_STROKE_ALPHA); + int br = HudRenderUtil.setAlpha(HudRenderUtil.mixColor(theme.accentSoft(), theme.accent(), 0.58f), BetterTooltips.TOOLTIP_STROKE_ALPHA); + int bl = HudRenderUtil.setAlpha(HudRenderUtil.mixColor(theme.accentSoft(), theme.accent(), 0.24f), BetterTooltips.TOOLTIP_STROKE_ALPHA); + GuiGraphicsExtractor ctx = TooltipState.getCtx(); + ViewportContext.beginScaled(ctx); + boolean rendered = renderScripted(mc, Renderer2D.COLOR, tr, x, y, totalW, totalH, bg, tl, trc, br, bl, glow, BetterTooltips.TOOLTIP_STROKE_WIDTH, BetterTooltips.TOOLTIP_GLOW_SIZE, 1.0f, instance.tooltipPanelHandle(), instance.tooltipRuntime(), ctx); + if (!rendered) drawFallback(x, y, totalW, totalH, bg, tl, trc, br, bl, glow); + int tx = x + BetterTooltips.TOOLTIP_PAD_X, ty = y + BetterTooltips.TOOLTIP_PAD_Y; + tr.begin(scale, false, false); + for (int i = 0; i < finalLines.size(); i++) { + TooltipLine l = finalLines.get(i); + tr.render(l.text(), tx, ty, new RenderColor(l.color()), true); + ty += lh; + if (i == 0 && finalLines.size() > 1) ty += BetterTooltips.TOOLTIP_TITLE_GAP; else if (i + 1 < finalLines.size()) ty += BetterTooltips.TOOLTIP_LINE_GAP; + } + tr.end(); + ViewportContext.end(ctx); + if (preview != null) { + int cw = totalW - BetterTooltips.TOOLTIP_PAD_X * 2; + int px = x + BetterTooltips.TOOLTIP_PAD_X + Math.max(0, (cw - preview.width()) / 2); + int py = y + BetterTooltips.TOOLTIP_PAD_Y + textH + BetterTooltips.TOOLTIP_PREVIEW_GAP; + ShulkerPreviewRenderer.render(preview, px, py, ctx); + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tooltips/state/ShulkerPreviewCache.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tooltips/state/ShulkerPreviewCache.java new file mode 100644 index 0000000..fa0fbe3 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tooltips/state/ShulkerPreviewCache.java @@ -0,0 +1,126 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.nondraggable.impl.tooltips; + +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.core.NonNullList; +import net.minecraft.core.component.DataComponents; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.component.ItemContainerContents; +import net.minecraft.world.level.block.ShulkerBoxBlock; + +final class ShulkerPreviewCache { + + static final int SHULKER_HINT_COLOR = 0xFFFFAA00; + private static final float PADDING = 4.0f; + private static final float SLOT_GAP = 2.0f; + + private static PreviewLayout cache; + private static int cacheSignature; + private static float cachePad; + private static float cacheSlotSize; + private static float cacheSlotGap; + private static boolean cacheHasItems; + + private ShulkerPreviewCache() { + } + + static PreviewLayout buildLayout() { + BetterTooltips self = BetterTooltips.get(); + if (self == null || !self.isShulkerPreviewEnabled() || !self.isShulkerPreviewHeld()) return null; + ItemStack toolStack = TooltipState.getStack(); + GuiGraphicsExtractor ctx = TooltipState.getCtx(); + if (toolStack == null || toolStack.isEmpty() || !isShulkerBox(toolStack) || ctx == null) return null; + ItemContainerContents container = toolStack.get(DataComponents.CONTAINER); + if (container == null) return null; + int cols = 9; + int rows = 3; + NonNullList stacks = NonNullList.withSize(cols * rows, ItemStack.EMPTY); + container.copyInto(stacks); + float pad = PADDING; + float slotSize = self.shulkerPreviewSlotSize(); + float gap = SLOT_GAP; + int sig = computeSignature(toolStack, stacks); + if (cache != null && cacheSignature == sig && Float.compare(cachePad, pad) == 0 + && Float.compare(cacheSlotSize, slotSize) == 0 && Float.compare(cacheSlotGap, gap) == 0) { + return cacheHasItems ? cache : null; + } + boolean hasItems = hasAnyStacks(stacks); + cacheSignature = sig; + cachePad = pad; + cacheSlotSize = slotSize; + cacheSlotGap = gap; + cacheHasItems = hasItems; + if (!hasItems) { + cache = null; + return null; + } + int bgW = Math.round(pad * 2f + cols * slotSize + (cols - 1) * gap); + int bgH = Math.round(pad * 2f + rows * slotSize + (rows - 1) * gap); + cache = new PreviewLayout(bgW, bgH, cols, rows, pad, slotSize, gap, copyStacks(stacks), sig); + return cache; + } + + static boolean isShulkerBox(ItemStack stack) { + if (stack == null || stack.isEmpty()) return false; + if (!(stack.getItem() instanceof BlockItem bi)) return false; + return bi.getBlock() instanceof ShulkerBoxBlock; + } + + static boolean hasShulkerContents(ItemStack stack) { + if (!isShulkerBox(stack)) return false; + ItemContainerContents container = stack.get(DataComponents.CONTAINER); + if (container == null) return false; + for (net.minecraft.world.item.ItemStackTemplate t : container.nonEmptyItems()) { + ItemStack s = t.create(); + if (s != null && !s.isEmpty()) return true; + } + return false; + } + + static boolean hasAnyStacks(NonNullList stacks) { + if (stacks == null) return false; + for (ItemStack s : stacks) { + if (s != null && !s.isEmpty()) return true; + } + return false; + } + + static NonNullList copyStacks(NonNullList stacks) { + NonNullList copy = NonNullList.withSize(stacks.size(), ItemStack.EMPTY); + for (int i = 0; i < stacks.size(); i++) { + ItemStack s = stacks.get(i); + copy.set(i, s == null ? ItemStack.EMPTY : s.copy()); + } + return copy; + } + + static int computeSignature(ItemStack toolStack, NonNullList stacks) { + int hash = 1; + hash = 31 * hash + (toolStack == null ? 0 : toolStack.getItem().hashCode()); + hash = 31 * hash + stacks.size(); + for (int i = 0; i < stacks.size(); i++) { + ItemStack s = stacks.get(i); + if (s == null || s.isEmpty()) { + hash = 31 * hash; + continue; + } + hash = 31 * hash + s.getItem().hashCode(); + hash = 31 * hash + s.getCount(); + hash = 31 * hash + s.getDamageValue(); + hash = 31 * hash + s.getComponents().hashCode(); + } + return hash; + } +} + +record PreviewLayout(int width, int height, int cols, int rows, float pad, + float slotSize, float slotGap, NonNullList stacks, + int containerSignature) { +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tooltips/state/TooltipState.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tooltips/state/TooltipState.java new file mode 100644 index 0000000..06f4e7b --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tooltips/state/TooltipState.java @@ -0,0 +1,199 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.nondraggable.impl.tooltips; + +import dev.loki.lovisual.features.theme.Theme; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipPositioner; +import net.minecraft.client.gui.screens.inventory.tooltip.DefaultTooltipPositioner; +import net.minecraft.util.FormattedCharSequence; +import net.minecraft.world.item.ItemStack; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +final class TooltipState { + + private static List lines; + private static ClientTooltipPositioner positioner; + private static int mouseX; + private static int mouseY; + private static ItemStack stack; + private static GuiGraphicsExtractor ctx; + private static float scaleOverride = 1.0f; + private static ItemStack lastStack; + + private TooltipState() {} + static void beginFrame() { + lines = null; + positioner = null; + stack = null; + ctx = null; + lastStack = null; + scaleOverride = 1.0f; + } + + static boolean hasTooltip() { + return lines != null && !lines.isEmpty(); + } + + static void captureOrdered(List src, + ClientTooltipPositioner pos, int x, int y) { + if (src == null || src.isEmpty()) return; + lines = convertOrdered(src); + positioner = pos != null ? pos : DefaultTooltipPositioner.INSTANCE; + mouseX = x; + mouseY = y; + stack = ItemStack.EMPTY; + } + + static void captureItemOrdered(List src, + ClientTooltipPositioner pos, int x, int y, + ItemStack s, GuiGraphicsExtractor c) { + if (src == null || src.isEmpty()) return; + lines = convertOrdered(src); + positioner = pos != null ? pos : DefaultTooltipPositioner.INSTANCE; + mouseX = x; + mouseY = y; + stack = s; + ctx = c; + } + + static void setDrawContext(GuiGraphicsExtractor c) { + ctx = c; + } + + static void setLastStack(ItemStack s) { + lastStack = s; + } + + static ItemStack consumeLastStack() { + ItemStack s = lastStack; + lastStack = null; + return s; + } + + static void setScaleOverride(float scale) { + scaleOverride = Math.max(0.5f, Math.min(1.5f, scale)); + } + + static void resetScaleOverride() { + scaleOverride = 1.0f; + } + + static List getLines() { + return lines; + } + + static ClientTooltipPositioner getPositioner() { + return positioner; + } + + static int getMouseX() { + return mouseX; + } + + static int getMouseY() { + return mouseY; + } + + static ItemStack getStack() { + return stack; + } + + static GuiGraphicsExtractor getCtx() { + return ctx; + } + + static float getScaleOverride() { + return scaleOverride; + } + + static List convertOrdered(List src) { + List out = new ArrayList<>(); + int fallback = Theme.theme().textPrimary(); + for (FormattedCharSequence line : src) { + StringBuilder sb = new StringBuilder(); + int[] color = new int[]{0}; + line.accept((idx, style, cp) -> { + sb.appendCodePoint(cp); + if (color[0] == 0 && style != null && style.getColor() != null) { + color[0] = 0xFF000000 | style.getColor().getValue(); + } + return true; + }); + if (sb.length() == 0) sb.append(' '); + int c = color[0] != 0 ? color[0] : fallback; + out.add(new TooltipLine(sb.toString(), c)); + } + return out; + } + + static List prepareLines(List in, boolean previewOpen) { + if (in == null || in.isEmpty()) return in; + boolean isShulker = ShulkerPreviewCache.isShulkerBox(stack); + boolean showHint = shouldShowHint(isShulker); + if (!previewOpen && !showHint) return in; + List out = new ArrayList<>(in.size() + (showHint ? 1 : 0)); + for (TooltipLine line : in) { + if (previewOpen && isShulker && (isShulkerMoreLine(line.text()) || isShulkerItemLine(line.text()))) { + continue; + } + out.add(line); + } + if (showHint) { + out.add(new TooltipLine(buildHint(), ShulkerPreviewCache.SHULKER_HINT_COLOR)); + } + return out; + } + + private static boolean shouldShowHint(boolean isShulker) { + if (!isShulker) return false; + BetterTooltips b = BetterTooltips.get(); + if (b == null || !b.isShulkerPreviewEnabled()) return false; + if (!ShulkerPreviewCache.hasShulkerContents(stack)) return false; + return !b.isShulkerPreviewHeld(); + } + + private static String buildHint() { + String bind = BetterTooltips.get().getShulkerPreviewBind(); + return "<<" + bind + ">> показать содержимое"; + } + + private static boolean isShulkerMoreLine(String text) { + if (text == null) return false; + String t = text.trim(); + if (t.isEmpty()) return false; + String lower = t.toLowerCase(Locale.ROOT); + if (lower.startsWith("and ") || lower.startsWith("и ")) { + return t.contains("...") || t.contains("…"); + } + if (lower.startsWith("и еще") || lower.startsWith("и ещё")) { + return t.contains("...") || t.contains("…"); + } + return false; + } + + private static boolean isShulkerItemLine(String text) { + if (text == null) return false; + String t = text.trim(); + if (t.isEmpty()) return false; + int l = t.lastIndexOf('('); + int r = t.lastIndexOf(')'); + if (l < 0 || r <= l) return false; + for (int i = l + 1; i < r; i++) { + char c = t.charAt(i); + if (c >= '0' && c <= '9') return true; + } + return false; + } +} + +record TooltipLine(String text, int color) { +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/registry/HudElementAutoLoader.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/registry/HudElementAutoLoader.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/registry/HudElementAutoLoader.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/registry/HudElementAutoLoader.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/registry/HudElementInfo.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/registry/HudElementInfo.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/registry/HudElementInfo.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/registry/HudElementInfo.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/registry/HudElementRegister.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/registry/HudElementRegister.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/registry/HudElementRegister.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/registry/HudElementRegister.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/registry/HudElements.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/registry/HudElements.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/registry/HudElements.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/registry/HudElements.java diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/CompactHudStatModel.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/CompactHudStatModel.java new file mode 100644 index 0000000..a92ffb0 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/CompactHudStatModel.java @@ -0,0 +1,125 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.script; + +import java.util.LinkedHashMap; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.UiBounds; + +/** + * Mutable frame snapshot for compact stat widgets such as FPS, TPS, ping, and game time. + * Facade delegating to helpers in model/render subpackages (same package). + */ +public final class CompactHudStatModel { + private final String id; + private final Background background = new Background(); + private final Icon icon = new Icon(); + private final Divider divider = new Divider(); + private final TextRun value = new TextRun(); + private final TextRun unit = new TextRun(); + private final TextRun extra = new TextRun(); + private final Animation animation = new Animation(); + final LinkedHashMap data = new LinkedHashMap<>(); + private boolean visible; + private float rootX, rootY, width, height, radius, scale; + + public CompactHudStatModel(String id) { this.id = id != null ? id : ""; } + + public static String colorString(int argb) { return CompactHudCodec.colorString(argb); } + + public String id() { return id; } + public String getId() { return id; } + public boolean visible() { return visible; } + public float rootX() { return rootX; } + public float rootY() { return rootY; } + public float width() { return width; } + public float height() { return height; } + public float radius() { return radius; } + public float scale() { return scale; } + + public CompactHudStatModel setVisible(boolean v) { this.visible = v; return this; } + public CompactHudStatModel setRoot(float x, float y, float w, float h, float r, float s) { + this.rootX = x; this.rootY = y; this.width = w; this.height = h; this.radius = r; this.scale = s; return this; + } + + public Background background() { return background; } + public String backgroundEffect() { return background.effect; } + public float blurAlpha() { return background.blurAlpha; } + public Icon icon() { return icon; } + public Divider divider() { return divider; } + public TextRun value() { return value; } + public TextRun unit() { return unit; } + public TextRun extra() { return extra; } + public Animation animation() { return animation; } + + public CompactHudStatModel data(String k, Object v) { if (k != null && !k.isEmpty()) data.put(k, v); return this; } + LinkedHashMap dataMap() { return data; } + + public LinkedHashMap toProps() { + var p = new LinkedHashMap(72); + p.put("id", id); p.put("visible", visible); p.put("rootX", rootX); p.put("rootY", rootY); + p.put("width", width); p.put("height", height); p.put("radius", radius); p.put("scale", scale); + background.writeTo(p); icon.writeTo(p); divider.writeTo(p); + value.writeTo(p, "value"); unit.writeTo(p, "unit"); extra.writeTo(p, "extra"); + animation.writeTo(p); p.putAll(data); return p; + } + + public LinkedHashMap toTemplateProps() { + var p = toProps(); + float rv = CompactHudLayout.reservedValueWidth(this), ru = CompactHudLayout.reservedUnitWidth(this), re = CompactHudLayout.reservedExtraWidth(this); + p.put("width", width); p.put("valueW", rv); p.put("unitW", ru); p.put("extraW", re); + p.put("reservedValueWidth", rv); p.put("reservedUnitWidth", ru); p.put("reservedExtraWidth", re); + p.put("valueText", value.text); p.put("previousValue", animation.previousValue); + p.put("digitProgress", animation.digitProgress); p.put("digitOffset", animation.digitOffset); + p.put("effectTime", 0.0f); + p.put("xText", CompactHudLayout.str(this, "xText", "0")); p.put("yText", CompactHudLayout.str(this, "yText", "0")); + p.put("zText", CompactHudLayout.str(this, "zText", "0")); p.put("netherText", CompactHudLayout.str(this, "netherText", "")); + for (String k : new String[]{"fps","ping","tps","usedMiB","maxMiB","percent","bps","displayedBps","x","y","z","netherX","netherZ"}) p.remove(k); + return p; + } + + public long structuralSignature() { + long h = 0xcbf29ce484222325L; + h = CompactHudCodec.mix(h, id); h = CompactHudCodec.mix(h, visible); h = CompactHudCodec.mix(h, scale); + h = background.mixStructure(h); h = icon.mixStructure(h); h = divider.mixStructure(h); + h = value.mixStructure(h); h = unit.mixStructure(h); h = extra.mixStructure(h); + h = animation.mixStructure(h); h = CompactHudLayout.mixDataShape(this, h); return h; + } + + public long dynamicSignature() { + long h = 0xcbf29ce484222325L; + h = CompactHudCodec.mix(h, icon.color); h = CompactHudCodec.mix(h, divider.color); + h = value.mixDynamic(h); h = unit.mixDynamic(h); h = extra.mixDynamic(h); h = animation.mixDynamic(h); + for (var e : data.entrySet()) { h = CompactHudCodec.mix(h, e.getKey()); h = CompactHudCodec.mixObject(h, e.getValue()); } + return h; + } + + public long treeSignature() { return structuralSignature(); } + + public long layoutSignature(long s) { + long h = s; + h = CompactHudCodec.mix(h, rootX); h = CompactHudCodec.mix(h, rootY); + h = CompactHudCodec.mix(h, CompactHudLayout.reservedRootWidth(this)); + h = CompactHudCodec.mix(h, height); h = CompactHudCodec.mix(h, radius); + h = background.mixLayout(h); h = icon.mixLayout(h); h = divider.mixLayout(h); + h = value.mixLayout(h, CompactHudLayout.reservedValueWidth(this)); + h = unit.mixLayout(h, CompactHudLayout.reservedUnitWidth(this)); + h = extra.mixLayout(h, CompactHudLayout.reservedExtraWidth(this)); return h; + } + + public long layoutSignature(long s, long ignored) { return layoutSignature(s); } + public long layoutSignature() { return layoutSignature(structuralSignature()); } + + public LinkedHashMap> runtimePatches() { return CompactHudLayout.runtimePatches(this); } + public LinkedHashMap runtimeBoundsPatches() { return CompactHudLayout.runtimeBounds(this); } + + public static final class Background extends CompactHudParts.Background {} + public static final class Icon extends CompactHudParts.Icon {} + public static final class Divider extends CompactHudTextParts.Divider {} + public static final class TextRun extends CompactHudTextParts.TextRun {} + public static final class Animation extends CompactHudTextParts.Animation {} +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/script/HudScriptLayouts.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/HudScriptLayouts.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/script/HudScriptLayouts.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/HudScriptLayouts.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/script/ScriptableHudStatWidget.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/ScriptableHudStatWidget.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/script/ScriptableHudStatWidget.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/ScriptableHudStatWidget.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/script/ScriptedCompactHudStatRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/ScriptedCompactHudStatRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/script/ScriptedCompactHudStatRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/ScriptedCompactHudStatRenderer.java diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/model/CompactHudCodec.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/model/CompactHudCodec.java new file mode 100644 index 0000000..7158438 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/model/CompactHudCodec.java @@ -0,0 +1,65 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.script; + +final class CompactHudCodec { + private static final char[] HEX = "0123456789ABCDEF".toCharArray(); + + private CompactHudCodec() {} + + static float clamp01(float v) { + return Math.max(0.0f, Math.min(1.0f, v)); + } + + static String alpha(String hex, float a) { + int argb = parseColor(hex); + int na = Math.round(((argb >>> 24) & 0xFF) * clamp01(a)); + return color((argb & 0x00FFFFFF) | (na << 24)); + } + + static float alpha01(int argb) { + return ((argb >>> 24) & 0xFF) / 255.0f; + } + + static int parseColor(String hex) { + if (hex == null || hex.isBlank()) return 0xFFFFFFFF; + String t = hex.startsWith("#") ? hex.substring(1) : hex; + try { + if (t.length() == 6) return 0xFF000000 | Integer.parseUnsignedInt(t, 16); + if (t.length() == 8) return (int) Long.parseLong(t, 16); + } catch (NumberFormatException ignored) {} + return 0xFFFFFFFF; + } + + static String color(int argb) { + return colorString(argb); + } + + static String colorString(int argb) { + char[] out = new char[9]; + out[0] = '#'; + for (int i = 7; i >= 0; i--) out[8 - i] = HEX[(argb >>> (i * 4)) & 0xF]; + return new String(out); + } + + static long mix(long h, boolean v) { return mix(h, v ? 1 : 0); } + + static long mix(long h, int v) { h ^= v; return h * 0x100000001b3L; } + + static long mix(long h, float v) { return mix(h, Float.floatToIntBits(v)); } + + static long mix(long h, String v) { return mix(h, v != null ? v.hashCode() : 0); } + + static long mixObject(long h, Object v) { + if (v instanceof Boolean b) return mix(h, b); + if (v instanceof Integer i) return mix(h, i); + if (v instanceof Float f) return mix(h, f); + if (v instanceof Number n) return mix(h, Float.floatToIntBits(n.floatValue())); + return mix(h, v != null ? v.toString() : ""); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/model/CompactHudParts.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/model/CompactHudParts.java new file mode 100644 index 0000000..a102013 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/model/CompactHudParts.java @@ -0,0 +1,127 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.script; + +import java.util.LinkedHashMap; + +public final class CompactHudParts { + private CompactHudParts() {} + + public static class Background { + public String effect = "None"; + public boolean theme; + public float blurAlpha; + public int primary; + public int secondary; + public int stroke; + public float strokeWidth; + public float softness; + + public Background set(String effect, boolean theme, float blurAlpha, int primary, int secondary, int stroke, float strokeWidth, float softness) { + this.effect = effect != null ? effect : "None"; + this.theme = theme; + this.blurAlpha = blurAlpha; + this.primary = primary; + this.secondary = secondary; + this.stroke = stroke; + this.strokeWidth = strokeWidth; + this.softness = softness; + return this; + } + + void writeTo(LinkedHashMap p) { + p.put("backgroundEffect", effect); + p.put("backgroundTheme", theme); + p.put("blurAlpha", blurAlpha); + p.put("bgPrimary", CompactHudCodec.color(primary)); + p.put("bgSecondary", CompactHudCodec.color(secondary)); + p.put("stroke", CompactHudCodec.color(stroke)); + p.put("strokeWidth", strokeWidth); + p.put("softness", softness); + } + + long mix(long h) { + h = CompactHudCodec.mix(h, effect); + h = CompactHudCodec.mix(h, theme); + h = CompactHudCodec.mix(h, blurAlpha); + h = CompactHudCodec.mix(h, primary); + h = CompactHudCodec.mix(h, secondary); + h = CompactHudCodec.mix(h, stroke); + h = CompactHudCodec.mix(h, strokeWidth); + h = CompactHudCodec.mix(h, softness); + return h; + } + + long mixStructure(long h) { return CompactHudCodec.mix(h, effect); } + + long mixLayout(long h) { + h = CompactHudCodec.mix(h, strokeWidth); + h = CompactHudCodec.mix(h, softness); + return h; + } + } + + public static class Icon { + public boolean visible; + public String kind = ""; + public String id = ""; + public String glyph = ""; + public String font = ""; + public float x; + public float y; + public float width; + public float height; + public float scale; + public int color; + + public Icon texture(String id, float x, float y, float w, float h, int c) { + return set(true, "texture", id, "", "", x, y, w, h, 1.0f, c); + } + + public Icon glyph(String g, String f, float x, float y, float w, float h, float s, int c) { + return set(true, "glyph", "", g, f, x, y, w, h, s, c); + } + + public Icon hidden() { return set(false, "", "", "", "", 0, 0, 0, 0, 1.0f, 0); } + + private Icon set(boolean v, String k, String i, String g, String f, float x, float y, float w, float h, float s, int c) { + this.visible = v; this.kind = k != null ? k : ""; this.id = i != null ? i : ""; + this.glyph = g != null ? g : ""; this.font = f != null ? f : ""; + this.x = x; this.y = y; this.width = w; this.height = h; this.scale = s; this.color = c; + return this; + } + + void writeTo(LinkedHashMap p) { + p.put("iconVisible", visible); p.put("iconKind", kind); p.put("iconId", id); + p.put("iconGlyph", glyph); p.put("iconFont", font); p.put("iconX", x); + p.put("iconY", y); p.put("iconW", width); p.put("iconH", height); + p.put("iconScale", scale); p.put("iconColor", CompactHudCodec.color(color)); + } + + long mix(long h) { + h = CompactHudCodec.mix(h, visible); h = CompactHudCodec.mix(h, kind); + h = CompactHudCodec.mix(h, id); h = CompactHudCodec.mix(h, glyph); + h = CompactHudCodec.mix(h, font); h = CompactHudCodec.mix(h, x); + h = CompactHudCodec.mix(h, y); h = CompactHudCodec.mix(h, width); + h = CompactHudCodec.mix(h, height); h = CompactHudCodec.mix(h, scale); + return h; + } + + long mixStructure(long h) { + h = CompactHudCodec.mix(h, visible); h = CompactHudCodec.mix(h, kind); + h = CompactHudCodec.mix(h, id); h = CompactHudCodec.mix(h, glyph); + h = CompactHudCodec.mix(h, font); return h; + } + + long mixLayout(long h) { + h = CompactHudCodec.mix(h, x); h = CompactHudCodec.mix(h, y); + h = CompactHudCodec.mix(h, width); h = CompactHudCodec.mix(h, height); + h = CompactHudCodec.mix(h, scale); return h; + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/model/CompactHudTextParts.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/model/CompactHudTextParts.java new file mode 100644 index 0000000..3930550 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/model/CompactHudTextParts.java @@ -0,0 +1,109 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.script; + +import java.util.LinkedHashMap; + +public final class CompactHudTextParts { + private CompactHudTextParts() {} + + public static class Divider { + public boolean visible; + public float x; public float y; public float width; public float height; public int color; + + public Divider set(float x, float y, float w, float h, int c) { + this.visible = true; this.x = x; this.y = y; this.width = w; this.height = h; this.color = c; return this; + } + + void writeTo(LinkedHashMap p) { + p.put("dividerVisible", visible); p.put("dividerX", x); p.put("dividerY", y); + p.put("dividerW", width); p.put("dividerH", height); + p.put("dividerColor", CompactHudCodec.color(color)); + } + + long mix(long h) { + h = CompactHudCodec.mix(h, visible); h = CompactHudCodec.mix(h, x); + h = CompactHudCodec.mix(h, y); h = CompactHudCodec.mix(h, width); + h = CompactHudCodec.mix(h, height); return h; + } + long mixStructure(long h) { return CompactHudCodec.mix(h, visible); } + long mixLayout(long h) { + h = CompactHudCodec.mix(h, x); h = CompactHudCodec.mix(h, y); + h = CompactHudCodec.mix(h, width); h = CompactHudCodec.mix(h, height); return h; + } + } + + public static class TextRun { + public boolean visible; + public String text = ""; + public String font = ""; + public float scale; + public float x; public float y; public float width; + public int color; + + public TextRun set(String text, String font, float scale, float x, float y, float w, int c) { + this.visible = text != null && !text.isEmpty(); + this.text = text != null ? text : ""; this.font = font != null ? font : ""; + this.scale = scale; this.x = x; this.y = y; this.width = w; this.color = c; return this; + } + + public TextRun hidden() { + this.visible = false; this.text = ""; this.font = ""; this.scale = 1.0f; + this.x = 0; this.y = 0; this.width = 0; this.color = 0; return this; + } + + void writeTo(LinkedHashMap p, String prefix) { + p.put(prefix + "Visible", visible); p.put(prefix + "Text", text); + p.put(prefix + "Font", font); p.put(prefix + "Scale", scale); + p.put(prefix + "X", x); p.put(prefix + "Y", y); + p.put(prefix + "W", width); p.put(prefix + "Color", CompactHudCodec.color(color)); + } + + long mixStructure(long h) { + h = CompactHudCodec.mix(h, visible); h = CompactHudCodec.mix(h, font); h = CompactHudCodec.mix(h, scale); return h; + } + long mixLayout(long h, float rw) { return CompactHudCodec.mix(h, rw); } + long mixDynamic(long h) { + h = CompactHudCodec.mix(h, visible); h = CompactHudCodec.mix(h, text); h = CompactHudCodec.mix(h, color); return h; + } + } + + public static class Animation { + public String labelEffect = "NONE"; + public int labelEffectSpeed; + public float effectTime; + public boolean digitAnimation; + public String previousValue = ""; + public float digitProgress = 1.0f; + public float digitOffset; + + public Animation setLabelEffect(String e, int s, float t) { + this.labelEffect = e != null ? e : "NONE"; this.labelEffectSpeed = s; this.effectTime = t; return this; + } + + public Animation setDigitAnimation(boolean en, String prev, float pr, float off) { + this.digitAnimation = en; this.previousValue = prev != null ? prev : ""; + this.digitProgress = pr; this.digitOffset = off; return this; + } + + void writeTo(LinkedHashMap p) { + p.put("labelEffect", labelEffect); p.put("labelEffectSpeed", labelEffectSpeed); + p.put("effectTime", effectTime); p.put("digitAnimation", digitAnimation); + p.put("previousValue", previousValue); p.put("digitProgress", digitProgress); + p.put("digitOffset", digitOffset); + } + + long mixStructure(long h) { + h = CompactHudCodec.mix(h, labelEffect); h = CompactHudCodec.mix(h, labelEffectSpeed); h = CompactHudCodec.mix(h, digitAnimation); return h; + } + long mixDynamic(long h) { + h = CompactHudCodec.mix(h, effectTime); h = CompactHudCodec.mix(h, previousValue); + h = CompactHudCodec.mix(h, digitProgress); h = CompactHudCodec.mix(h, digitOffset); return h; + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/render/CompactHudLayout.java b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/render/CompactHudLayout.java new file mode 100644 index 0000000..0a22b28 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/gui/hud/script/render/CompactHudLayout.java @@ -0,0 +1,175 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.script; + +import java.util.LinkedHashMap; +import dev.loki.lovisual.render.engine.renderer.ui.runtime.core.UiBounds; + +final class CompactHudLayout { + private CompactHudLayout() {} + + static float reservedRootWidth(CompactHudStatModel m) { + float s = m.scale() != 0 ? m.scale() : 1.0f; + return switch (m.id()) { + case "fps" -> (26 + 36 + 2 + 17 + 5) * s; + case "ping" -> (26 + 40 + 2 + 16 + 5) * s; + case "tps" -> (26 + 39 + 2 + 15 + 3.5f) * s; + case "memory" -> (26 + 74 + 2 + 17 + 5 + 34 + 5) * s; + case "speed_bps" -> (26 + 36 + 2 + 20 + 3.5f) * s; + case "game_time" -> (28.5f + 45 + 2 + 18 + 5) * s; + case "system_time" -> (28.5f + 66 + 2 + 18 + 5) * s; + case "xyz" -> (bool(m, "showNether") ? 150 : 112) * s; + default -> Math.max(m.width(), (26 + 42 + 2 + 18 + 5) * s); + }; + } + + static float reservedValueWidth(CompactHudStatModel m) { + float s = m.scale() != 0 ? m.scale() : 1.0f; + float slot = switch (m.id()) { + case "fps" -> 36; case "ping" -> 40; case "tps" -> 39; case "memory" -> 74; + case "speed_bps" -> 36; case "game_time" -> 45; case "system_time" -> 66; + case "xyz" -> 28; default -> 42; + }; + return Math.max(m.value().width, slot * s); + } + + static float reservedUnitWidth(CompactHudStatModel m) { + float s = m.scale() != 0 ? m.scale() : 1.0f; + float slot = switch (m.id()) { + case "fps", "memory" -> 17; case "ping" -> 16; case "tps" -> 15; case "speed_bps" -> 20; default -> 18; + }; + return Math.max(m.unit().width, slot * s); + } + + static float reservedExtraWidth(CompactHudStatModel m) { + float s = m.scale() != 0 ? m.scale() : 1.0f; + float slot = switch (m.id()) { + case "memory" -> 34; case "xyz" -> bool(m, "showNether") ? 34 : 0; default -> 0; + }; + return Math.max(m.extra().width, slot * s); + } + + static float visualWidth(CompactHudStatModel m) { + if ("xyz".equals(m.id()) && bool(m, "showNether")) return Math.max(1.0f, m.width() - 4.0f * m.scale()); + return m.width(); + } + + static long mixDataShape(CompactHudStatModel m, long h) { + for (var e : m.dataMap().entrySet()) { + String k = e.getKey(); Object v = e.getValue(); + h = CompactHudCodec.mix(h, k); + if (v instanceof Boolean b) h = CompactHudCodec.mix(h, b); + else if ("labelColor".equals(k) || "valueColor".equals(k)) h = CompactHudCodec.mixObject(h, v); + else if ("netherText".equals(k)) h = CompactHudCodec.mix(h, v != null && !v.toString().isEmpty()); + else if (v != null) h = CompactHudCodec.mix(h, v.getClass().getName()); + else h = CompactHudCodec.mix(h, 0); + } + return h; + } + + static LinkedHashMap> runtimePatches(CompactHudStatModel m) { + var patches = new LinkedHashMap>(12); + putProp(patches, "compact-stat:" + m.id(), "renderBlurAlpha", m.background().blurAlpha); + putProp(patches, "fill", "startColor", CompactHudCodec.color(m.background().primary)); + putProp(patches, "fill", "endColor", CompactHudCodec.color(m.background().secondary)); + float pa = Math.max(CompactHudCodec.alpha01(m.background().primary), CompactHudCodec.alpha01(m.background().secondary)); + float sa = Math.max(CompactHudCodec.alpha01(m.background().stroke), pa); + putProp(patches, "fill", "stroke", CompactHudCodec.alpha(CompactHudCodec.color(m.background().stroke), sa)); + putProp(patches, "fill", "strokeStartColor", CompactHudCodec.alpha("#FFFFFFFF", 0.16f * sa)); + putProp(patches, "fill", "strokeEndColor", CompactHudCodec.alpha(CompactHudCodec.color(m.background().stroke), sa)); + putProp(patches, "top-glint", "startColor", CompactHudCodec.alpha("#FFFFFFFF", 0.065f * pa)); + putProp(patches, "top-glint", "endColor", "#00000000"); + putProp(patches, "icon:texture", "tint", CompactHudCodec.color(m.icon().color)); + putProp(patches, "icon:glyph", "color", CompactHudCodec.color(m.icon().color)); + putProp(patches, "divider", "fill", CompactHudCodec.color(m.divider().color)); + putText(patches, "value", m.value().text, CompactHudCodec.color(m.value().color)); + putText(patches, "unit", m.unit().text, CompactHudCodec.color(m.unit().color)); + putText(patches, "extra", m.extra().text, CompactHudCodec.color(m.extra().color)); + float pr = CompactHudCodec.clamp01(m.animation().digitProgress); + float off = m.animation().digitOffset; + putText(patches, "digit:current", m.value().text, CompactHudCodec.alpha(CompactHudCodec.color(m.value().color), pr), -off * (1 - pr) + 2.0f); + putText(patches, "digit:prev", m.animation().previousValue, CompactHudCodec.alpha(CompactHudCodec.color(m.value().color), 1 - pr), off * pr + 2.0f); + putText(patches, "x:v", str(m, "xText", "0"), str(m, "valueColor", CompactHudCodec.color(m.value().color))); + putText(patches, "y:v", str(m, "yText", "0"), str(m, "valueColor", CompactHudCodec.color(m.value().color))); + putText(patches, "z:v", str(m, "zText", "0"), str(m, "valueColor", CompactHudCodec.color(m.value().color))); + putText(patches, "nether", str(m, "netherText", ""), CompactHudCodec.color(m.extra().color)); + return patches; + } + + static LinkedHashMap runtimeBounds(CompactHudStatModel m) { + var patches = new LinkedHashMap(24); + float vw = visualWidth(m); + putBounds(patches, "compact-stat:" + m.id(), m.rootX(), m.rootY(), vw, m.height()); + putBounds(patches, "clip", m.rootX(), m.rootY(), vw, m.height()); + putBounds(patches, "fill", m.rootX(), m.rootY(), vw, m.height()); + putBounds(patches, "top-glint", m.rootX() + 1, m.rootY() + 1, Math.max(0, vw - 2), Math.max(1, m.height() * 0.42f)); + if (m.icon().visible) { + putBounds(patches, "icon:texture", m.rootX() + m.icon().x, m.rootY() + m.icon().y, m.icon().width, m.icon().height); + putBounds(patches, "icon:glyph", m.rootX() + m.icon().x, m.rootY() + m.icon().y, m.icon().width, m.icon().height); + } + if (m.divider().visible) putBounds(patches, "divider", m.rootX() + m.divider().x, m.rootY() + m.divider().y, m.divider().width, m.divider().height); + if ("xyz".equals(m.id())) putCoords(m, patches, vw); + else if (m.animation().digitAnimation) { + putBounds(patches, "digit:layer", m.rootX(), m.rootY(), m.width(), m.height()); + putBounds(patches, "digit:clip", m.rootX() + m.value().x, m.rootY() + m.value().y - 2, m.value().width, Math.max(8, m.height())); + putBounds(patches, "digit:prev", m.rootX() + m.value().x, m.rootY() + m.value().y - 2, m.value().width, Math.max(8, m.height())); + putBounds(patches, "digit:current", m.rootX() + m.value().x, m.rootY() + m.value().y - 2, m.value().width, Math.max(8, m.height())); + putBounds(patches, "unit", m.unit().visible ? m.rootX() + m.unit().x : m.rootX(), m.unit().visible ? m.rootY() + m.unit().y : m.rootY(), m.unit().visible ? m.unit().width : 0, m.unit().visible ? Math.max(8, m.height()) : 0); + putBounds(patches, "extra", m.extra().visible ? m.rootX() + m.extra().x : m.rootX(), m.extra().visible ? m.rootY() + m.extra().y : m.rootY(), m.extra().visible ? m.extra().width : 0, m.extra().visible ? Math.max(8, m.height()) : 0); + } else { + putBounds(patches, "text:layer", m.rootX(), m.rootY(), m.width(), m.height()); + putBounds(patches, "value", m.value().visible ? m.rootX() + m.value().x : m.rootX(), m.value().visible ? m.rootY() + m.value().y : m.rootY(), m.value().visible ? m.value().width : 0, m.value().visible ? Math.max(8, m.height()) : 0); + putBounds(patches, "unit", m.unit().visible ? m.rootX() + m.unit().x : m.rootX(), m.unit().visible ? m.rootY() + m.unit().y : m.rootY(), m.unit().visible ? m.unit().width : 0, m.unit().visible ? Math.max(8, m.height()) : 0); + putBounds(patches, "extra", m.extra().visible ? m.rootX() + m.extra().x : m.rootX(), m.extra().visible ? m.rootY() + m.extra().y : m.rootY(), m.extra().visible ? m.extra().width : 0, m.extra().visible ? Math.max(8, m.height()) : 0); + } + return patches; + } + + private static void putCoords(CompactHudStatModel m, LinkedHashMap p, float vw) { + float rx = m.rootX() + m.value().x, ry = m.rootY() + m.value().y, rh = Math.max(8, m.height() - m.value().y); + float pg = Math.max(1, m.scale() * 1.6f), g = Math.max(2, m.scale() * 3.2f); + float lxw = num(m, "labelXW", 6 * m.scale()), lyw = num(m, "labelYW", lxw), lzw = num(m, "labelZW", lxw); + float vxw = num(m, "valueXW", 28 * m.scale()), vyw = num(m, "valueYW", vxw), vzw = num(m, "valueZW", vxw); + float ew = num(m, "xyzExtraW", m.extra().width); + float c = rx; + putBounds(p, "coords:row", rx, ry, Math.max(0, vw - m.value().x - 4), rh); + putBounds(p, "x:l", c, ry, lxw, rh); c += lxw + pg; + putBounds(p, "x:v", c, ry, vxw, rh); c += vxw + g; + putBounds(p, "y:l", c, ry, lyw, rh); c += lyw + pg; + putBounds(p, "y:v", c, ry, vyw, rh); c += vyw + g; + putBounds(p, "z:l", c, ry, lzw, rh); c += lzw + pg; + putBounds(p, "z:v", c, ry, vzw, rh); c += vzw + g; + if (bool(m, "showNether")) putBounds(p, "nether", c, ry, ew, rh); + } + + private static void putBounds(LinkedHashMap p, String k, float x, float y, float w, float h) { + p.put(k, new UiBounds(x, y, Math.max(0, w), Math.max(0, h))); + } + + private static void putProp(LinkedHashMap> p, String k, String prop, Object v) { + var m = p.get(k); if (m == null) { m = new LinkedHashMap<>(2); p.put(k, m); } m.put(prop, v); + } + + private static void putText(LinkedHashMap> p, String k, String t, String c) { putText(p, k, t, c, Float.NaN); } + + private static void putText(LinkedHashMap> p, String k, String t, String c, float off) { + var m = new LinkedHashMap(3); m.put("text", t != null ? t : ""); + if (c != null) m.put("color", c); if (!Float.isNaN(off)) m.put("textOffsetY", off); p.put(k, m); + } + + static boolean bool(CompactHudStatModel m, String k) { Object v = m.dataMap().get(k); return v instanceof Boolean b && b; } + + static float num(CompactHudStatModel m, String k, float fb) { + Object v = m.dataMap().get(k); + if (v instanceof Number n) return n.floatValue(); + if (v instanceof String s) try { return Float.parseFloat(s); } catch (NumberFormatException ignored) {} + return fb; + } + + static String str(CompactHudStatModel m, String k, String fb) { Object v = m.dataMap().get(k); return v != null ? String.valueOf(v) : fb; } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/mainmenu/LoVisualAddonManagerScreen.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/LoVisualAddonManagerScreen.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/mainmenu/LoVisualAddonManagerScreen.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/LoVisualAddonManagerScreen.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/mainmenu/LoVisualMainMenuScreen.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/LoVisualMainMenuScreen.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/mainmenu/LoVisualMainMenuScreen.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/LoVisualMainMenuScreen.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/mainmenu/LoVisualProxyManagerScreen.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/LoVisualProxyManagerScreen.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/mainmenu/LoVisualProxyManagerScreen.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/LoVisualProxyManagerScreen.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/AltAccountOps.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/AltAccountOps.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/AltAccountOps.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/AltAccountOps.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/AltManagerController.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/AltManagerController.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/AltManagerController.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/AltManagerController.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/AltPanelAnims.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/AltPanelAnims.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/AltPanelAnims.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/AltPanelAnims.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/LoVisualAltManagerScreen.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/LoVisualAltManagerScreen.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/LoVisualAltManagerScreen.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/LoVisualAltManagerScreen.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/auth/AltMicrosoftAuth.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/auth/AltMicrosoftAuth.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/auth/AltMicrosoftAuth.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/auth/AltMicrosoftAuth.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/AltFieldsRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/AltFieldsRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/AltFieldsRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/AltFieldsRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/AltManagerRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/AltManagerRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/AltManagerRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/AltManagerRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/AltPanelsRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/AltPanelsRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/AltPanelsRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/AltPanelsRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/AltUiMetrics.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/AltUiMetrics.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/AltUiMetrics.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/AltUiMetrics.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/cards/AltCardRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/cards/AltCardRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/cards/AltCardRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/cards/AltCardRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/cards/AltCardsRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/cards/AltCardsRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/cards/AltCardsRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/gui/mainmenu/alt/render/cards/AltCardsRenderer.java diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/core/Module.java b/mod/src/main/java/dev/loki/lovisual/features/module/core/Module.java new file mode 100644 index 0000000..4ae488f --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/core/Module.java @@ -0,0 +1,139 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ +package dev.loki.lovisual.features.module.core; + +import com.mojang.blaze3d.vertex.PoseStack; +import dev.loki.lovisual.config.common.CommonBooleanGroupSchema; +import dev.loki.lovisual.config.common.CommonSettingSchema; +import dev.loki.lovisual.config.definition.ConfigNameProvider; +import dev.loki.lovisual.config.definition.ConfigObject; +import dev.loki.lovisual.config.definition.SettingDef; +import dev.loki.lovisual.config.definition.SettingOwner; +import dev.loki.lovisual.config.i18n.ConfigValueAliasProvider; +import dev.loki.lovisual.config.values.collection.ConfigValue; +import dev.loki.lovisual.config.values.collection.ItemIdSetValue; +import dev.loki.lovisual.config.values.collection.SetValue; +import dev.loki.lovisual.config.values.color.KeyBindValue; +import dev.loki.lovisual.config.values.color.RGBAColorValue; +import dev.loki.lovisual.config.values.color.RGBColorValue; +import dev.loki.lovisual.config.values.mode.BindMode; +import dev.loki.lovisual.config.values.mode.EnumValue; +import dev.loki.lovisual.config.values.mode.ModeValue; +import dev.loki.lovisual.config.values.primitive.BooleanMapValue; +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import dev.loki.lovisual.config.values.primitive.StringValue; +import dev.loki.lovisual.events.impl.render.RenderPrewarmCollectEvent; +import dev.loki.lovisual.features.gui.clickgui.settings.bind.FunctionBindSetting; +import dev.loki.lovisual.features.gui.clickgui.settings.bind.KeyBindSetting; +import dev.loki.lovisual.features.gui.clickgui.settings.core.Setting; +import dev.loki.lovisual.features.gui.clickgui.settings.text.list.TextListSetting; +import dev.loki.lovisual.features.module.lifecycle.ModuleActivationSource; +import dev.loki.lovisual.features.module.module.ModuleNames; +import dev.loki.lovisual.features.module.phase.HudPhase; +import dev.loki.lovisual.features.module.phase.WorldPhase; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import dev.loki.lovisual.render.engine.text.font.info.FontInfo; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.renderer.SubmitNodeCollector; +import java.util.*; +import java.util.function.Supplier; + +public abstract class Module implements ConfigObject, ConfigNameProvider, SettingOwner, ConfigValueAliasProvider { + protected final List settings = new ArrayList<>(); + private final String name; private final String displayName; private final List aliases; private final String description; private final ModuleCategory category; + private final ModuleSettingsHelper sh; private final ModuleBindHelper bh; private final ModuleLifecycleHelper lh; private final ModuleConfigHelper ch; + + protected Module() { + ModuleInfo info = getClass().getAnnotation(ModuleInfo.class); + if (info == null) throw new IllegalStateException(getClass().getName() + " must be annotated with @ModuleInfo"); + this.name = info.id().toLowerCase(Locale.ROOT); this.displayName = info.displayName(); this.aliases = ModuleNames.normalizeAliases(info.aliases()); + this.description = info.description(); this.category = info.category(); + this.ch = new ModuleConfigHelper(this, info.enabledByDefault()); this.sh = new ModuleSettingsHelper(this); this.bh = new ModuleBindHelper(this); this.lh = new ModuleLifecycleHelper(this); + settings.add(bh.keyBind()); + } + @Deprecated public Module(String n,String dn,ModuleCategory c){this(n,dn,c,"");} + @Deprecated public Module(String n,String dn,ModuleCategory c,String d){ this.name=n.toLowerCase(Locale.ROOT); this.displayName=dn; this.aliases=List.of(); this.description=d==null?"":d; this.category=c; this.ch=new ModuleConfigHelper(this,false); this.sh=new ModuleSettingsHelper(this); this.bh=new ModuleBindHelper(this); this.lh=new ModuleLifecycleHelper(this); settings.add(bh.keyBind()); } + + public final void postInit(){ sh.postInit(settings); ModuleManager.register(this); } + @Deprecated protected void loadSettings(List s){} + + protected final BooleanValue bool(String cn,boolean d){return sh.bool(cn,d);} protected final BooleanValue bool(String cn,String sid,boolean d){return sh.bool(cn,sid,d);} + protected final NumberValue num(String cn,int d,int min,int max){return sh.num(cn,d,min,max);} protected final NumberValue num(String cn,String sid,int d,int min,int max){return sh.num(cn,sid,d,min,max);} + protected final NumberValue num(String cn,long d,long min,long max){return sh.numL(cn,d,min,max);} protected final NumberValue num(String cn,String sid,long d,long min,long max){return sh.numL(cn,sid,d,min,max);} + protected final NumberValue num(String cn,float d,float min,float max){return sh.numF(cn,d,min,max);} protected final NumberValue num(String cn,String sid,float d,float min,float max){return sh.numF(cn,sid,d,min,max);} + protected final NumberValue num(String cn,double d,double min,double max){return sh.numD(cn,d,min,max);} protected final NumberValue num(String cn,String sid,double d,double min,double max){return sh.numD(cn,sid,d,min,max);} + protected final ModeValue mode(String cn,String d,String... o){return sh.mode(cn,d,o);} protected final ModeValue modeSetting(String cn,String sid,String d,String... o){return sh.modeSetting(cn,sid,d,o);} + protected final > EnumValue enumMode(String cn,E d){return sh.enumMode(cn,d);} protected final > EnumValue enumSetting(String cn,String sid,E d){return sh.enumSetting(cn,sid,d);} + @Deprecated protected final > EnumValue enumMode(String cn,E d,Class c){return sh.enumModeCl(cn,d,c);} @Deprecated protected final > EnumValue enumSetting(String cn,String sid,E d,Class c){return sh.enumSettingCl(cn,sid,d,c);} + @SafeVarargs protected final > EnumValue enumMode(String cn,E d,E... o){return sh.enumModeArr(cn,d,o);} @SafeVarargs protected final > EnumValue enumSetting(String cn,String sid,E d,E... o){return sh.enumSettingArr(cn,sid,d,o);} + protected final RGBAColorValue color(String cn,String h){return sh.color(cn,h);} protected final RGBAColorValue color(String cn,String sid,String h){return sh.color(cn,sid,h);} + protected final RGBColorValue colorNoAlpha(String cn,String h){return sh.colorNoAlpha(cn,h);} protected final RGBColorValue colorNoAlpha(String cn,String sid,String h){return sh.colorNoAlpha(cn,sid,h);} + protected final StringValue text(String cn,String d){return sh.text(cn,d);} protected final StringValue text(String cn,String sid,String d){return sh.text(cn,sid,d);} + protected final SetValue textList(String cn){return sh.textList(cn);} protected final SetValue textList(String cn,TextListSetting.PickerMode m){return sh.textList(cn,m);} + protected final SetValue textList(String cn,String sid,TextListSetting.PickerMode m){return sh.textList(cn,sid,m);} protected final SetValue textList(String cn,String sid,TextListSetting.PickerMode m,Set defs){return sh.textList(cn,sid,m,defs);} + protected final ItemIdSetValue itemList(String cn){return sh.itemList(cn);} protected final ItemIdSetValue itemList(String cn,TextListSetting.PickerMode m){return sh.itemList(cn,m);} + protected final ItemIdSetValue itemList(String cn,String sid,TextListSetting.PickerMode m){return sh.itemList(cn,sid,m);} + protected final BooleanMapValue group(String cn,Map d){return sh.group(cn,d);} protected final BooleanMapValue group(String cn,String sid,Map d){return sh.group(cn,sid,d);} + protected final KeyBindValue bind(String cn,String d,BindMode m){return sh.bind(cn,d,m);} protected final KeyBindValue bind(String cn,String sid,String d,BindMode m){return sh.bind(cn,sid,d,m);} + protected final BooleanMapValue groupCommon(String cn,String k,Map d){return sh.groupCommon(cn,k,d);} protected final BooleanMapValue groupCommon(String cn,String sid,String k,Map d){return sh.groupCommon(cn,sid,k,d);} + protected final ModeValue modeCommon(String cn,String k,String d,String... o){return sh.modeCommon(cn,k,d,o);} protected final ModeValue modeCommon(String cn,String sid,String k,String d,String... o){return sh.modeCommon(cn,sid,k,d,o);} + protected final > EnumValue enumCommon(String cn,String k,E d){return sh.enumCommon(cn,k,d);} protected final > EnumValue enumCommon(String cn,String sid,String k,E d){return sh.enumCommon(cn,sid,k,d);} + protected final > EnumValue enumCommon(String cn,String k,E d,Class c){return sh.enumCommonCl(cn,k,d,c);} protected final > EnumValue enumCommon(String cn,String sid,String k,E d,Class c){return sh.enumCommonCl(cn,sid,k,d,c);} + @SafeVarargs protected final > EnumValue enumCommon(String cn,String k,E d,E... o){return sh.enumCommonArr(cn,k,d,o);} @SafeVarargs protected final > EnumValue enumCommon(String cn,String sid,String k,E d,E... o){return sh.enumCommonArr(cn,sid,k,d,o);} + protected final NumberValue numCommon(String cn,String k,int d,int min,int max){return sh.numCommon(cn,k,d,min,max);} protected final NumberValue numCommon(String cn,String sid,String k,int d,int min,int max){return sh.numCommon(cn,sid,k,d,min,max);} + protected final NumberValue numCommon(String cn,String k,float d,float min,float max){return sh.numCommonF(cn,k,d,min,max);} protected final NumberValue numCommon(String cn,String sid,String k,float d,float min,float max){return sh.numCommonF(cn,sid,k,d,min,max);} + protected final NumberValue numCommon(String cn,String k,double d,double min,double max){return sh.numCommonD(cn,k,d,min,max);} protected final NumberValue numCommon(String cn,String sid,String k,double d,double min,double max){return sh.numCommonD(cn,sid,k,d,min,max);} + protected final BooleanValue boolCommon(String cn,String k,boolean d){return sh.boolCommon(cn,k,d);} protected final BooleanValue boolCommon(String cn,String sid,String k,boolean d){return sh.boolCommon(cn,sid,k,d);} + protected final BooleanMapValue groupCommon(String cn,CommonBooleanGroupSchema s){return sh.groupCommon(cn,s);} protected final BooleanMapValue groupCommon(String cn,String sid,CommonBooleanGroupSchema s){return sh.groupCommon(cn,sid,s);} + protected final BooleanMapValue protocolHeuristics(String cn,String sid,CommonBooleanGroupSchema s){return sh.protocolHeuristics(cn,sid,s);} + protected final > EnumValue enumCommon(String cn,CommonSettingSchema s,E d){return sh.enumCommon(cn,s,d);} protected final > EnumValue enumCommon(String cn,String sid,CommonSettingSchema s,E d){return sh.enumCommon(cn,sid,s,d);} + protected final > EnumValue enumCommon(String cn,CommonSettingSchema s,E d,Class c){return sh.enumCommonCl(cn,s,d,c);} protected final > EnumValue enumCommon(String cn,String sid,CommonSettingSchema s,E d,Class c){return sh.enumCommonCl(cn,sid,s,d,c);} + @SafeVarargs protected final > EnumValue enumCommon(String cn,CommonSettingSchema s,E d,E... o){return sh.enumCommonArr(cn,s,d,o);} @SafeVarargs protected final > EnumValue enumCommon(String cn,String sid,CommonSettingSchema s,E d,E... o){return sh.enumCommonArr(cn,sid,s,d,o);} + protected final ModeValue modeCommon(String cn,CommonSettingSchema s,String d,String... o){return sh.modeCommon(cn,s,d,o);} protected final ModeValue modeCommon(String cn,String sid,CommonSettingSchema s,String d,String... o){return sh.modeCommon(cn,sid,s,d,o);} + protected final NumberValue numCommon(String cn,CommonSettingSchema s,int d,int min,int max){return sh.numCommon(cn,s,d,min,max);} protected final NumberValue numCommon(String cn,String sid,CommonSettingSchema s,int d,int min,int max){return sh.numCommon(cn,sid,s,d,min,max);} + protected final NumberValue numCommon(String cn,CommonSettingSchema s,float d,float min,float max){return sh.numCommonF(cn,s,d,min,max);} protected final NumberValue numCommon(String cn,String sid,CommonSettingSchema s,float d,float min,float max){return sh.numCommonF(cn,sid,s,d,min,max);} + protected final NumberValue numCommon(String cn,CommonSettingSchema s,double d,double min,double max){return sh.numCommonD(cn,s,d,min,max);} protected final NumberValue numCommon(String cn,String sid,CommonSettingSchema s,double d,double min,double max){return sh.numCommonD(cn,sid,s,d,min,max);} + protected final BooleanValue boolCommon(String cn,CommonSettingSchema s,boolean d){return sh.boolCommon(cn,s,d);} protected final BooleanValue boolCommon(String cn,String sid,CommonSettingSchema s,boolean d){return sh.boolCommon(cn,sid,s,d);} + protected final > V visibleWhen(V v,Supplier s){return sh.visibleWhen(v,s);} protected final > V shownWhen(V v,Supplier s){return sh.shownWhen(v,s);} + protected final > V unavailableWhen(V v,Supplier s,String r){return sh.unavailableWhen(v,s,r);} protected final > V unavailableWhen(V v,Supplier s,Supplier r){return sh.unavailableWhen(v,s,r);} + protected final > V notAppliedWhen(V v,Supplier s,String r){return sh.notAppliedWhen(v,s,r);} protected final > V notAppliedWhen(V v,Supplier s,Supplier r){return sh.notAppliedWhen(v,s,r);} + protected final > V appliesWhen(V v,Supplier s,String r){return sh.appliesWhen(v,s,r);} protected final > V appliesWhen(V v,Supplier s,Supplier r){return sh.appliesWhen(v,s,r);} + protected final SettingDef setting(SettingDef d){return sh.setting(d);} public final void addExtensionSetting(SettingDef d){sh.addExtension(d,settings);} + protected final > V common(V v,String k){return sh.common(v,k);} protected final > V common(V v,CommonSettingSchema s){return sh.common(v,s);} + + @Override public List getLegacyValueNames(String n){return ch.legacyNames(n);} + public List getSettings(){return settings;} public ConfigValue getConfigValue(String n){ if(n==null||n.isBlank()) return null; for(ConfigValue v: getConfigValues()) if(v!=null&&n.equals(v.getName())) return v; return null; } + public KeyBindSetting getKeyBindSetting(){return bh.keyBind();} public Collection getActionSettings(){return bh.actionSettings();} public void setDefaultBind(String k){bh.setDefaultBind(k);} + protected final FunctionBindSetting action(String n,String d,BindMode m){return bh.action(n,d,m);} protected final FunctionBindSetting action(String n,String d){return bh.action(n,d);} + protected final void addAction(String n,String d,BindMode m){bh.addAction(n,d,m);} protected final void addAction(String n,String d){bh.addAction(n,d);} + protected final boolean isActionPressedOnce(String n){return bh.isPressedOnce(n,lh.isEnabled());} protected final boolean isActionHeld(String n){return bh.isHeld(n,lh.isEnabled());} protected final String getActionKey(String n){return bh.actionKey(n);} + public boolean isShownInModuleList(){return ch.isShown();} public void setShownInModuleList(boolean v){ch.setShown(v);} public void toggleShownInModuleList(){ch.toggleShown();} + public boolean isEnabled(){return lh.isEnabled();} protected String getUnavailableReason(){return "";} public final boolean isAvailable(){return lh.isAvailable();} public final String getAvailabilityReason(){return lh.getReason();} + public final void setEnabled(boolean s){setEnabled(s,ModuleActivationSource.MANUAL);} public ModuleActivationSource getActivationSource(){return lh.activationSource();} + public final void setActivationSource(ModuleActivationSource s){lh.setSource(s,ch.activationValue(),ch.enabledValue());} public boolean isEnabledFromKeybind(){return lh.isFromKeybind();} + public final void setEnabledFromKeybind(boolean s){setEnabled(s,ModuleActivationSource.KEYBIND);} public String name(){return name;} @Override public String getConfigName(){return name();} + public String getDisplayName(){return ModuleNames.translateFirst(displayName,"module."+name(),"module."+name()+".name");} public List getAliases(){return aliases;} + public String getDescription(){return ModuleNames.translateFirst(description,"module."+name()+".description");} public ModuleCategory getCategory(){return category;} + public final void toggle(){setEnabled(!isEnabled(),ModuleActivationSource.MANUAL);} public final void toggleFromKeybind(){setEnabled(!isEnabled(),ModuleActivationSource.KEYBIND);} + public final void setEnabled(boolean state,ModuleActivationSource src){lh.setEnabled(state,src,ch.enabledValue(),ch.activationValue());} + public final void setEnabledFromConfig(boolean s){setEnabled(s,ModuleActivationSource.CONFIG);} public final void setEnabledInternal(boolean s){setEnabled(s,ModuleActivationSource.INTERNAL);} public final void setEnabledManual(boolean s){setEnabled(s,ModuleActivationSource.MANUAL);} + final void setRuntimeEnabledTransient(boolean s){lh.setTransient(s,ch.enabledValue(),ch.activationValue());} + public void collectRenderPrewarm(RenderPrewarmCollectEvent e){ if(e==null) return; e.font("Onest",FontInfo.Type.Regular).font("OnestMedium",FontInfo.Type.Regular).font("OnestBold",FontInfo.Type.Regular).font("Inter",FontInfo.Type.Regular).font("Inter",FontInfo.Type.Bold).font("OnestMedium",FontInfo.Type.Regular).font("Iosevka",FontInfo.Type.Regular).font("Iosevka",FontInfo.Type.Bold); } + public void onTick(){} public void onFrame(float d){} public void onKey(int k,int a){} public void onEnable(){} public void onDisable(){} + public void onRender2D(GuiGraphicsExtractor c){} public void onRender2D(GuiGraphicsExtractor c,float d){onRender2D(c);} + public void onRenderHudEngine(Renderer2D r,TextRenderer t){} public void onRenderHudEngine(Renderer2D r,TextRenderer t,float d){onRenderHudEngine(r,t);} public void onRenderHudEngine(Renderer2D r,TextRenderer t,GuiGraphicsExtractor c,float d){onRenderHudEngine(r,t,d);} + public void onRenderHudEngineForeground(Renderer2D r,TextRenderer t,GuiGraphicsExtractor c,float d){} + public void onRenderWorld(PoseStack m,SubmitNodeCollector co){} public void onRenderWorld(PoseStack m,SubmitNodeCollector co,float d){onRenderWorld(m,co);} + public void onRenderWorldEngine(Renderer3D r,Renderer3D d){} public void onRenderWorldEngine(Renderer3D r,Renderer3D d,float t){onRenderWorldEngine(r,d);} + public HudPhase getHudPhase(){return HudPhase.NONE;} public WorldPhase getWorldPhase(){return WorldPhase.NONE;} + @Override public List> getConfigValues(){return ch.ordered(settings);} + public void applyLegacySettings(Object j){ch.applyLegacy(j,settings);} public void syncSettingsAfterLoad(){ch.sync(settings);} public void applyConfiguredEnabledState(){setEnabled(ch.enabledValue().get(),ch.activationValue().get());} + public void loadAndApplyConfig(){ch.loadAndApply(lh);} public void saveConfig(){ch.requestSave();} +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/core/ModuleCategory.java b/mod/src/main/java/dev/loki/lovisual/features/module/core/ModuleCategory.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/core/ModuleCategory.java rename to mod/src/main/java/dev/loki/lovisual/features/module/core/ModuleCategory.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/core/ModuleInfo.java b/mod/src/main/java/dev/loki/lovisual/features/module/core/ModuleInfo.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/core/ModuleInfo.java rename to mod/src/main/java/dev/loki/lovisual/features/module/core/ModuleInfo.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/core/ModuleManager.java b/mod/src/main/java/dev/loki/lovisual/features/module/core/ModuleManager.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/core/ModuleManager.java rename to mod/src/main/java/dev/loki/lovisual/features/module/core/ModuleManager.java diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/core/ext/ModuleBindHelper.java b/mod/src/main/java/dev/loki/lovisual/features/module/core/ext/ModuleBindHelper.java new file mode 100644 index 0000000..129f56f --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/core/ext/ModuleBindHelper.java @@ -0,0 +1,39 @@ +package dev.loki.lovisual.features.module.core; + +import dev.loki.lovisual.config.values.collection.ConfigValue; +import dev.loki.lovisual.features.gui.clickgui.settings.bind.FunctionBindSetting; +import dev.loki.lovisual.features.gui.clickgui.settings.bind.KeyBindSetting; +import dev.loki.lovisual.features.gui.clickgui.settings.core.Setting; +import dev.loki.lovisual.features.module.core.Module; +import dev.loki.lovisual.features.module.module.ModuleBinds; + +import java.util.*; + +final class ModuleBindHelper { + private final Module owner; + private final KeyBindSetting keyBind; + private final Map actions = new LinkedHashMap<>(); + + ModuleBindHelper(Module owner) { + this.owner = owner; + this.keyBind = ModuleBinds.createKeyBindSetting(); + } + + KeyBindSetting keyBind() { return keyBind; } + Map actions() { return actions; } + Collection actionSettings() { return Collections.unmodifiableCollection(actions.values()); } + + void setDefaultBind(String key) { keyBind.setKeyByName(key); } + FunctionBindSetting action(String name, String def, dev.loki.lovisual.config.values.mode.BindMode mode) { return ModuleBinds.registerAction(actions, owner.getSettings(), name, def, mode); } + FunctionBindSetting action(String name, String def) { return action(name, def, dev.loki.lovisual.config.values.mode.BindMode.PRESS); } + void addAction(String name, String def, dev.loki.lovisual.config.values.mode.BindMode mode) { action(name, def, mode); } + void addAction(String name, String def) { action(name, def); } + boolean isPressedOnce(String name, boolean enabled) { return ModuleBinds.isPressedOnce(enabled, actions.get(name)); } + boolean isHeld(String name, boolean enabled) { return ModuleBinds.isHeld(enabled, actions.get(name)); } + String actionKey(String name) { return ModuleBinds.actionKey(actions.get(name)); } + ConfigValue findConfig(String name, List settings) { + if (name==null||name.isBlank()) return null; + for (Setting s: settings) { ConfigValue v=s.getConfigValue(); if(v!=null&&name.equals(v.getName())) return v; } + return null; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/core/ext/ModuleConfigHelper.java b/mod/src/main/java/dev/loki/lovisual/features/module/core/ext/ModuleConfigHelper.java new file mode 100644 index 0000000..7bc326e --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/core/ext/ModuleConfigHelper.java @@ -0,0 +1,46 @@ +package dev.loki.lovisual.features.module.core; + +import dev.loki.lovisual.config.definition.ConfigObject; +import dev.loki.lovisual.config.io.ConfigSerializer; +import dev.loki.lovisual.config.values.collection.ConfigValue; +import dev.loki.lovisual.config.values.mode.EnumValue; +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.features.gui.clickgui.settings.core.Setting; +import dev.loki.lovisual.features.module.core.Module; +import dev.loki.lovisual.features.module.lifecycle.ModuleActivationSource; +import dev.loki.lovisual.features.module.module.ModuleConfigValues; +import dev.loki.lovisual.util.logging.DebugLog; + +import java.util.List; + +final class ModuleConfigHelper { + private final Module owner; + private final BooleanValue enabledValue; + private final EnumValue activationSourceValue; + private final BooleanValue visibleValue; + + ModuleConfigHelper(Module owner, boolean defEnabled) { + this.owner = owner; + this.enabledValue = new BooleanValue("enabled", defEnabled); + this.activationSourceValue = new EnumValue<>("activation_source", ModuleActivationSource.NONE, ModuleActivationSource.class); + this.visibleValue = new BooleanValue(ModuleConfigValues.SETTING_SHOW_IN_MODULE_LIST, true); + } + + BooleanValue enabledValue(){return enabledValue;} + EnumValue activationValue(){return activationSourceValue;} + BooleanValue visibleValue(){return visibleValue;} + + List> ordered(List settings){return ModuleConfigValues.orderedValues(enabledValue, activationSourceValue, visibleValue, settings);} + List legacyNames(String n){ if(ModuleConfigValues.SETTING_SHOW_IN_MODULE_LIST.equals(n)) return List.of(ModuleConfigValues.LEGACY_SETTING_SHOW_IN_ACTIVE_MODULES); return List.of();} + void applyLegacy(Object json, List s){ModuleConfigValues.applyLegacy(json, visibleValue, s);} + void sync(List s){ModuleConfigValues.syncAfterLoad(s);} + void requestSave(){ModuleConfigValues.requestSaveQuietly(owner);} + boolean isShown(){return visibleValue.get();} + void setShown(boolean v){ if(visibleValue.get()==v) return; visibleValue.set(v); requestSave();} + void toggleShown(){setShown(!visibleValue.get());} + + void loadAndApply(ModuleLifecycleHelper lh){ + try{ DebugLog.config("Module config load -> %s", owner.name()); ConfigSerializer.load((ConfigObject)owner);}catch(Throwable e){DebugLog.error("Module config load failed for %s", e, owner.name());} + lh.loadAndApply(enabledValue, activationSourceValue); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/core/ext/ModuleLifecycleHelper.java b/mod/src/main/java/dev/loki/lovisual/features/module/core/ext/ModuleLifecycleHelper.java new file mode 100644 index 0000000..62df25d --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/core/ext/ModuleLifecycleHelper.java @@ -0,0 +1,71 @@ +package dev.loki.lovisual.features.module.core; + +import dev.loki.lovisual.Lifecycle; +import dev.loki.lovisual.addon.manager.ModuleExtensionManager; +import dev.loki.lovisual.config.values.mode.EnumValue; +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.features.module.core.Module; +import dev.loki.lovisual.features.module.core.ModuleManager; +import dev.loki.lovisual.features.module.lifecycle.ModuleActivationSource; + +final class ModuleLifecycleHelper { + private final Module owner; + private boolean enabled; + private ModuleActivationSource source = ModuleActivationSource.NONE; + + ModuleLifecycleHelper(Module owner) { this.owner = owner; } + + boolean isEnabled() { return enabled; } + ModuleActivationSource activationSource() { return source; } + + static ModuleActivationSource normalize(boolean state, ModuleActivationSource s) { + if (!state) return ModuleActivationSource.NONE; + return s==null||s==ModuleActivationSource.NONE ? ModuleActivationSource.INTERNAL : s; + } + + boolean isAvailable() { return getReason().isEmpty(); } + String getReason() { try{String r=owner.getUnavailableReason(); return r==null?"":r.trim();}catch(Exception e){return ""; } } + + void setEnabled(boolean state, ModuleActivationSource src, BooleanValue ev, EnumValue av) { + if (state && !Lifecycle.isActive()) return; + ModuleActivationSource norm = normalize(state, src); + if (enabled==state) { + if (source==norm && ev.get()==enabled && av.get()==source) return; + source=norm; ev.set(enabled); av.set(source); + dev.loki.lovisual.features.module.module.ModuleConfigValues.requestSaveQuietly(owner); return; + } + source=norm; enabled=state; + if (enabled) { + if (!ModuleExtensionManager.beforeEnable(owner)) { enabled=false; source=ModuleActivationSource.NONE; return; } + owner.onEnable(); ModuleExtensionManager.afterEnable(owner); + } else { + if (!ModuleExtensionManager.beforeDisable(owner)) { enabled=true; source=norm; return; } + owner.onDisable(); ModuleExtensionManager.afterDisable(owner); + } + ModuleManager.notifyListeners(owner.name(), enabled); + ev.set(enabled); av.set(source); + dev.loki.lovisual.features.module.module.ModuleConfigValues.requestSaveQuietly(owner); + } + + void setTransient(boolean state, BooleanValue ev, EnumValue av) { + if (enabled==state) return; enabled=state; + if (enabled) { + if (ModuleExtensionManager.beforeEnable(owner)) { owner.onEnable(); ModuleExtensionManager.afterEnable(owner);} else enabled=false; + } else if (ModuleExtensionManager.beforeDisable(owner)) { owner.onDisable(); ModuleExtensionManager.afterDisable(owner);} else enabled=true; + ModuleManager.notifyListeners(owner.name(), enabled); + } + + void loadAndApply(BooleanValue ev, EnumValue av) { + enabled=ev.get(); source=normalize(enabled, av.get()); av.set(source); + if (enabled) { + if (ModuleExtensionManager.beforeEnable(owner)) { owner.onEnable(); ModuleExtensionManager.afterEnable(owner);} + else { enabled=false; source=ModuleActivationSource.NONE; ev.set(false); av.set(source); } + } + } + + void setSource(ModuleActivationSource src, EnumValue av, BooleanValue ev) { + source=normalize(enabled, src); av.set(source); + dev.loki.lovisual.features.module.module.ModuleConfigValues.requestSaveQuietly(owner); + } + boolean isFromKeybind(){return enabled && source==ModuleActivationSource.KEYBIND;} +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/core/ext/ModuleSettingsHelper.java b/mod/src/main/java/dev/loki/lovisual/features/module/core/ext/ModuleSettingsHelper.java new file mode 100644 index 0000000..db4d5d8 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/core/ext/ModuleSettingsHelper.java @@ -0,0 +1,134 @@ +package dev.loki.lovisual.features.module.core; + +import dev.loki.lovisual.config.common.CommonBooleanGroupSchema; +import dev.loki.lovisual.config.common.CommonSettingSchema; +import dev.loki.lovisual.config.definition.SettingDef; +import dev.loki.lovisual.config.values.collection.ConfigValue; +import dev.loki.lovisual.config.values.collection.ItemIdSetValue; +import dev.loki.lovisual.config.values.collection.SetValue; +import dev.loki.lovisual.config.values.color.KeyBindValue; +import dev.loki.lovisual.config.values.color.RGBAColorValue; +import dev.loki.lovisual.config.values.color.RGBColorValue; +import dev.loki.lovisual.config.values.mode.BindMode; +import dev.loki.lovisual.config.values.mode.EnumValue; +import dev.loki.lovisual.config.values.mode.ModeValue; +import dev.loki.lovisual.config.values.primitive.BooleanMapValue; +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import dev.loki.lovisual.config.values.primitive.StringValue; +import dev.loki.lovisual.features.gui.clickgui.settings.core.Setting; +import dev.loki.lovisual.features.gui.clickgui.settings.core.SettingFactory; +import dev.loki.lovisual.features.gui.clickgui.settings.text.list.TextListSetting; +import dev.loki.lovisual.features.module.core.Module; +import dev.loki.lovisual.features.module.module.ModuleNames; +import dev.loki.lovisual.features.module.module.ModuleSettingValues; + +import java.util.*; +import java.util.function.Supplier; + +final class ModuleSettingsHelper { + private final Module owner; + private final List declared = new ArrayList<>(); + private final Map, SettingDef> byValue = new IdentityHashMap<>(); + + ModuleSettingsHelper(Module owner) { this.owner = owner; } + + List declared() { return declared; } + Map, SettingDef> byValue() { return byValue; } + + void postInit(List settings) { + SettingDef.applyI18nAnnotations(owner, declared); + settings.addAll(SettingFactory.fromDefs(declared)); + owner.loadSettings(settings); + for (Setting s : settings) { s.setParent(owner); s.preflightI18n(); } + } + + private void declare(ConfigValue v, SettingDef d) { declared.add(d); byValue.put(v, d); } + private String sid(String cn) { return ModuleNames.settingId(cn, owner.name(), owner.getClass().getSimpleName()); } + + BooleanValue bool(String cn, boolean def) { return bool(cn, sid(cn), def); } + BooleanValue bool(String cn, String sid, boolean def) { return ModuleSettingValues.bool(cn, sid, def, this::declare); } + NumberValue num(String cn, int def, int min, int max) { return num(cn, sid(cn), def, min, max); } + NumberValue num(String cn, String sid, int def, int min, int max) { return ModuleSettingValues.num(cn, sid, def, min, max, this::declare); } + NumberValue numL(String cn, long def, long min, long max) { return numL(cn, sid(cn), def, min, max); } + NumberValue numL(String cn, String sid, long def, long min, long max) { return ModuleSettingValues.num(cn, sid, def, min, max, this::declare); } + NumberValue numF(String cn, float def, float min, float max) { return numF(cn, sid(cn), def, min, max); } + NumberValue numF(String cn, String sid, float def, float min, float max) { return ModuleSettingValues.num(cn, sid, def, min, max, this::declare); } + NumberValue numD(String cn, double def, double min, double max) { return numD(cn, sid(cn), def, min, max); } + NumberValue numD(String cn, String sid, double def, double min, double max) { return ModuleSettingValues.num(cn, sid, def, min, max, this::declare); } + ModeValue mode(String cn, String def, String... opts) { return modeSetting(cn, sid(cn), def, opts); } + ModeValue modeSetting(String cn, String sid, String def, String... opts) { return ModuleSettingValues.modeSetting(cn, sid, def, this::declare, opts); } + > EnumValue enumMode(String cn, E def) { return enumSetting(cn, sid(cn), def); } + > EnumValue enumSetting(String cn, String sid, E def) { return ModuleSettingValues.enumSetting(cn, sid, def, this::declare); } + @Deprecated > EnumValue enumModeCl(String cn, E def, Class c) { return enumSettingCl(cn, sid(cn), def, c); } + @Deprecated > EnumValue enumSettingCl(String cn, String sid, E def, Class c) { return ModuleSettingValues.enumSetting(cn, sid, def, c, this::declare); } + @SafeVarargs final > EnumValue enumModeArr(String cn, E def, E... opts) { return enumSettingArr(cn, sid(cn), def, opts); } + @SafeVarargs final > EnumValue enumSettingArr(String cn, String sid, E def, E... opts) { return ModuleSettingValues.enumSetting(cn, sid, def, this::declare, opts); } + RGBAColorValue color(String cn, String hex) { return color(cn, sid(cn), hex); } + RGBAColorValue color(String cn, String sid, String hex) { return ModuleSettingValues.color(cn, sid, hex, this::declare); } + RGBColorValue colorNoAlpha(String cn, String hex) { return colorNoAlpha(cn, sid(cn), hex); } + RGBColorValue colorNoAlpha(String cn, String sid, String hex) { return ModuleSettingValues.colorNoAlpha(cn, sid, hex, this::declare); } + StringValue text(String cn, String def) { return text(cn, sid(cn), def); } + StringValue text(String cn, String sid, String def) { return ModuleSettingValues.text(cn, sid, def, this::declare); } + SetValue textList(String cn) { return textList(cn, sid(cn), TextListSetting.PickerMode.ALL); } + SetValue textList(String cn, TextListSetting.PickerMode m) { return textList(cn, sid(cn), m); } + SetValue textList(String cn, String sid, TextListSetting.PickerMode m) { return ModuleSettingValues.textList(cn, sid, m, this::declare); } + SetValue textList(String cn, String sid, TextListSetting.PickerMode m, Set defs) { return ModuleSettingValues.textList(cn, sid, m, defs, this::declare); } + ItemIdSetValue itemList(String cn) { return itemList(cn, sid(cn), TextListSetting.PickerMode.ITEMS); } + ItemIdSetValue itemList(String cn, TextListSetting.PickerMode m) { return itemList(cn, sid(cn), m); } + ItemIdSetValue itemList(String cn, String sid, TextListSetting.PickerMode m) { return ModuleSettingValues.itemList(cn, sid, m, this::declare); } + BooleanMapValue group(String cn, Map defs) { return group(cn, sid(cn), defs); } + BooleanMapValue group(String cn, String sid, Map defs) { return ModuleSettingValues.group(cn, sid, defs, this::declare); } + KeyBindValue bind(String cn, String def, BindMode mode) { return bind(cn, sid(cn), def, mode); } + KeyBindValue bind(String cn, String sid, String def, BindMode mode) { return ModuleSettingValues.bind(cn, sid, def, mode, this::declare); } + BooleanMapValue groupCommon(String cn, String key, Map defs) { return groupCommon(cn, sid(cn), key, defs); } + BooleanMapValue groupCommon(String cn, String sid, String key, Map defs) { return common(group(cn, sid, defs), key); } + ModeValue modeCommon(String cn, String key, String def, String... opts) { return modeCommon(cn, sid(cn), key, def, opts); } + ModeValue modeCommon(String cn, String sid, String key, String def, String... opts) { return common(modeSetting(cn, sid, def, opts), key); } + > EnumValue enumCommon(String cn, String key, E def) { return enumCommon(cn, sid(cn), key, def); } + > EnumValue enumCommon(String cn, String sid, String key, E def) { return common(enumSetting(cn, sid, def), key); } + > EnumValue enumCommonCl(String cn, String key, E def, Class c) { return enumCommonCl(cn, sid(cn), key, def, c); } + > EnumValue enumCommonCl(String cn, String sid, String key, E def, Class c) { return common(enumSettingCl(cn, sid, def, c), key); } + @SafeVarargs final > EnumValue enumCommonArr(String cn, String key, E def, E... opts) { return enumCommonArr(cn, sid(cn), key, def, opts); } + @SafeVarargs final > EnumValue enumCommonArr(String cn, String sid, String key, E def, E... opts) { return common(enumSettingArr(cn, sid, def, opts), key); } + NumberValue numCommon(String cn, String key, int def, int min, int max) { return numCommon(cn, sid(cn), key, def, min, max); } + NumberValue numCommon(String cn, String sid, String key, int def, int min, int max) { return common(num(cn, sid, def, min, max), key); } + NumberValue numCommonF(String cn, String key, float def, float min, float max) { return numCommonF(cn, sid(cn), key, def, min, max); } + NumberValue numCommonF(String cn, String sid, String key, float def, float min, float max) { return common(numF(cn, sid, def, min, max), key); } + NumberValue numCommonD(String cn, String key, double def, double min, double max) { return numCommonD(cn, sid(cn), key, def, min, max); } + NumberValue numCommonD(String cn, String sid, String key, double def, double min, double max) { return common(numD(cn, sid, def, min, max), key); } + BooleanValue boolCommon(String cn, String key, boolean def) { return boolCommon(cn, sid(cn), key, def); } + BooleanValue boolCommon(String cn, String sid, String key, boolean def) { return common(bool(cn, sid, def), key); } + BooleanMapValue groupCommon(String cn, CommonBooleanGroupSchema s) { return groupCommon(cn, sid(cn), s); } + BooleanMapValue groupCommon(String cn, String sid, CommonBooleanGroupSchema s) { if (s==null) return group(cn,sid,Map.of()); return common(group(cn,sid,s.defaults()), s); } + BooleanMapValue protocolHeuristics(String cn, String sid, CommonBooleanGroupSchema s) { return ModuleSettingValues.protocolHeuristics(cn, sid, s, this::declare); } + > EnumValue enumCommon(String cn, CommonSettingSchema s, E def) { return enumCommon(cn, sid(cn), s, def); } + > EnumValue enumCommon(String cn, String sid, CommonSettingSchema s, E def) { return common(enumSetting(cn, sid, def), s); } + > EnumValue enumCommonCl(String cn, CommonSettingSchema s, E def, Class c) { return enumCommonCl(cn, sid(cn), s, def, c); } + > EnumValue enumCommonCl(String cn, String sid, CommonSettingSchema s, E def, Class c) { return common(enumSettingCl(cn, sid, def, c), s); } + @SafeVarargs final > EnumValue enumCommonArr(String cn, CommonSettingSchema s, E def, E... opts) { return enumCommonArr(cn, sid(cn), s, def, opts); } + @SafeVarargs final > EnumValue enumCommonArr(String cn, String sid, CommonSettingSchema s, E def, E... opts) { return common(enumSettingArr(cn, sid, def, opts), s); } + ModeValue modeCommon(String cn, CommonSettingSchema s, String def, String... opts) { return modeCommon(cn, sid(cn), s, def, opts); } + ModeValue modeCommon(String cn, String sid, CommonSettingSchema s, String def, String... opts) { return common(modeSetting(cn, sid, def, opts), s); } + NumberValue numCommon(String cn, CommonSettingSchema s, int def, int min, int max) { return numCommon(cn, sid(cn), s, def, min, max); } + NumberValue numCommon(String cn, String sid, CommonSettingSchema s, int def, int min, int max) { return common(num(cn, sid, def, min, max), s); } + NumberValue numCommonF(String cn, CommonSettingSchema s, float def, float min, float max) { return numCommonF(cn, sid(cn), s, def, min, max); } + NumberValue numCommonF(String cn, String sid, CommonSettingSchema s, float def, float min, float max) { return common(numF(cn, sid, def, min, max), s); } + NumberValue numCommonD(String cn, CommonSettingSchema s, double def, double min, double max) { return numCommonD(cn, sid(cn), s, def, min, max); } + NumberValue numCommonD(String cn, String sid, CommonSettingSchema s, double def, double min, double max) { return common(numD(cn, sid, def, min, max), s); } + BooleanValue boolCommon(String cn, CommonSettingSchema s, boolean def) { return boolCommon(cn, sid(cn), s, def); } + BooleanValue boolCommon(String cn, String sid, CommonSettingSchema s, boolean def) { return common(bool(cn, sid, def), s); } + + > V visibleWhen(V v, Supplier s) { SettingDef d=byValue.get(v); if(d!=null) d.visibleWhen(s); return v; } + > V shownWhen(V v, Supplier s) { return visibleWhen(v,s); } + > V unavailableWhen(V v, Supplier s, String r) { SettingDef d=byValue.get(v); if(d!=null) d.unavailableWhen(s,r); return v; } + > V unavailableWhen(V v, Supplier s, Supplier r) { SettingDef d=byValue.get(v); if(d!=null) d.unavailableWhen(s,r); return v; } + > V notAppliedWhen(V v, Supplier s, String r) { return unavailableWhen(v,s,r); } + > V notAppliedWhen(V v, Supplier s, Supplier r) { return unavailableWhen(v,s,r); } + > V appliesWhen(V v, Supplier s, String r) { SettingDef d=byValue.get(v); if(d!=null) d.availableWhen(s,r); return v; } + > V appliesWhen(V v, Supplier s, Supplier r) { SettingDef d=byValue.get(v); if(d!=null) d.availableWhen(s,r); return v; } + SettingDef setting(SettingDef d) { if(d!=null&&d.value()!=null) declare(d.value(),d); return d; } + void addExtension(SettingDef d, List settings) { Setting s=SettingFactory.fromDef(d); if(s==null) return; s.setParent(owner); s.preflightI18n(); settings.add(s); if(d!=null&&d.value()!=null) byValue.put(d.value(),d); } + > V common(V v, String k) { SettingDef d=byValue.get(v); if(d!=null&&k!=null&&!k.isBlank()) d.common(k); return v; } + > V common(V v, CommonSettingSchema s) { SettingDef d=byValue.get(v); if(d!=null&&s!=null) d.common(s); return v; } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/lifecycle/ModuleActivationSource.java b/mod/src/main/java/dev/loki/lovisual/features/module/lifecycle/ModuleActivationSource.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/lifecycle/ModuleActivationSource.java rename to mod/src/main/java/dev/loki/lovisual/features/module/lifecycle/ModuleActivationSource.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/lifecycle/ModuleAutoLoader.java b/mod/src/main/java/dev/loki/lovisual/features/module/lifecycle/ModuleAutoLoader.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/lifecycle/ModuleAutoLoader.java rename to mod/src/main/java/dev/loki/lovisual/features/module/lifecycle/ModuleAutoLoader.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/lifecycle/ModuleRegister.java b/mod/src/main/java/dev/loki/lovisual/features/module/lifecycle/ModuleRegister.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/lifecycle/ModuleRegister.java rename to mod/src/main/java/dev/loki/lovisual/features/module/lifecycle/ModuleRegister.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/lifecycle/Modules.java b/mod/src/main/java/dev/loki/lovisual/features/module/lifecycle/Modules.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/lifecycle/Modules.java rename to mod/src/main/java/dev/loki/lovisual/features/module/lifecycle/Modules.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/module/ModuleBinds.java b/mod/src/main/java/dev/loki/lovisual/features/module/module/ModuleBinds.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/module/ModuleBinds.java rename to mod/src/main/java/dev/loki/lovisual/features/module/module/ModuleBinds.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/module/ModuleConfigValues.java b/mod/src/main/java/dev/loki/lovisual/features/module/module/ModuleConfigValues.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/module/ModuleConfigValues.java rename to mod/src/main/java/dev/loki/lovisual/features/module/module/ModuleConfigValues.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/module/ModuleNames.java b/mod/src/main/java/dev/loki/lovisual/features/module/module/ModuleNames.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/module/ModuleNames.java rename to mod/src/main/java/dev/loki/lovisual/features/module/module/ModuleNames.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/module/ModuleSettingValues.java b/mod/src/main/java/dev/loki/lovisual/features/module/module/ModuleSettingValues.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/module/ModuleSettingValues.java rename to mod/src/main/java/dev/loki/lovisual/features/module/module/ModuleSettingValues.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/combatlog/CombatLog.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/combatlog/CombatLog.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/combatlog/CombatLog.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/combatlog/CombatLog.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/combatlog/CombatLogEntry.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/combatlog/CombatLogEntry.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/combatlog/CombatLogEntry.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/combatlog/CombatLogEntry.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/combatlog/CombatLogStats.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/combatlog/CombatLogStats.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/combatlog/CombatLogStats.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/combatlog/CombatLogStats.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/damage/DamageTint.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/damage/DamageTint.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/damage/DamageTint.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/damage/DamageTint.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/damage/NoFriendDamage.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/damage/NoFriendDamage.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/damage/NoFriendDamage.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/damage/NoFriendDamage.java diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/Predictions.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/Predictions.java new file mode 100644 index 0000000..7e833a9 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/Predictions.java @@ -0,0 +1,146 @@ +package dev.loki.lovisual.features.module.modules.combat.prediction; + +import com.mojang.blaze3d.vertex.PoseStack; +import dev.loki.lovisual.config.values.color.RGBColorValue; +import dev.loki.lovisual.config.values.primitive.BooleanMapValue; +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import dev.loki.lovisual.features.module.core.Module; +import dev.loki.lovisual.features.module.core.ModuleCategory; +import dev.loki.lovisual.features.module.core.ModuleInfo; +import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsModel.TimerPlate; +import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsModel.TrajectoryResult; +import dev.loki.lovisual.features.module.phase.HudPhase; +import dev.loki.lovisual.features.module.phase.WorldPhase; +import dev.loki.lovisual.render.engine.color.RenderColor; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import dev.loki.lovisual.render.engine.text.font.info.FontInfo; +import dev.loki.lovisual.render.engine.text.font.Fonts; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.render.helpers.clip.ScreenProjection; +import dev.loki.lovisual.util.target.TargetManager; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.phys.Vec3; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@ModuleInfo(id = "predictions", displayName = "Predictions", category = ModuleCategory.COMBAT) +public class Predictions extends Module { + private static final String SETTING_LINE_COLOR = "line_color"; + private static final String SETTING_PEARL_CUSTOM_COLOR = "pearl_custom_color"; + private static final String SETTING_PEARL_COLOR = "pearl_color"; + private static final String SETTING_MAX_SIM_TICKS = "max_sim_ticks"; + private static final String SETTING_PEARL_MAX_SIM_TICKS = "pearl_max_ticks"; + private static final String SETTING_MAX_PROJECTILES = "max_projectiles"; + private static final String SETTING_PEARL_MAX_PROJECTILES = "pearl_max_projectiles"; + private static final String SETTING_SHOW_TIMER_PLATE = "show_plate"; + private static final String SETTING_AIM_TRAJECTORY = "aim_trajectory"; + private static final String SETTING_AIM_INDICATOR = "aim_indicator"; + private static final String SETTING_TARGETHUD_ON_AIM = "targethud_on_aim"; + private static final String SETTING_GLOW_WALLS = "glow_walls"; + private static final String SETTING_TNT_TIMER = "tnt_timer"; + private static final String SETTING_TNT_COLOR = "tnt_color"; + private static final String SETTING_PROJECTILES = "projectiles"; + + private final RGBColorValue lineColorValue = colorNoAlpha("pred_line_color", SETTING_LINE_COLOR, "#11C7C7"); + private final BooleanValue pearlColorToggle = bool("pred_pearl_custom_color", SETTING_PEARL_CUSTOM_COLOR, false); + private final RGBColorValue pearlColorValue = visibleWhen(colorNoAlpha("pred_pearl_color", SETTING_PEARL_COLOR, "#C68CFF"), pearlColorToggle::get); + private final NumberValue maxSimTicks = num("pred_max_ticks", SETTING_MAX_SIM_TICKS, 120, 20, 600); + private final NumberValue pearlMaxSimTicks = num("pred_pearl_max_ticks", SETTING_PEARL_MAX_SIM_TICKS, 160, 20, 1000); + private final NumberValue maxProjectiles = num("pred_max_projectiles", SETTING_MAX_PROJECTILES, 48, 1, 256); + private final NumberValue pearlMaxProjectiles = num("pred_pearl_max_projectiles", SETTING_PEARL_MAX_PROJECTILES, 12, 1, 128); + private final BooleanValue showTimerPlateValue = bool("pred_show_timer_plate", SETTING_SHOW_TIMER_PLATE, true); + private final BooleanValue aimTrajectoryValue = bool("pred_aim_trajectory", SETTING_AIM_TRAJECTORY, true); + private final BooleanValue aimIndicatorValue = bool("pred_aim_indicator", SETTING_AIM_INDICATOR, false); + private final BooleanValue targetHudOnAimValue = bool("pred_targethud_on_aim", SETTING_TARGETHUD_ON_AIM, false); + private final BooleanValue glowWallsValue = bool("pred_glow_walls", SETTING_GLOW_WALLS, false); + private final BooleanValue tntTimerValue = bool("pred_tnt_timer", SETTING_TNT_TIMER, true); + private final RGBColorValue tntColorValue = visibleWhen(colorNoAlpha("pred_tnt_color", SETTING_TNT_COLOR, "#FF5555"), tntTimerValue::get); + private final BooleanMapValue kindToggles = group("pred_kind_toggles", SETTING_PROJECTILES, new java.util.LinkedHashMap<>() {{ + put("arrow", true); put("trident", true); put("fireball", true); put("wind_charge", true); + put("firework", true); put("snowball", true); put("egg", true); + put("potion_splash", true); put("potion_lingering", true); + }}); + + private ItemStack tntTimerIcon = ItemStack.EMPTY; + private final List timerPlates = new ArrayList<>(); + + @Override public WorldPhase getWorldPhase() { return WorldPhase.END_MAIN; } + @Override public HudPhase getHudPhase() { return HudPhase.BEFORE_MISC_OVERLAYS; } + @Override public void onDisable() { TargetManager.setPredictionTarget(null); } + @Override public void onRenderWorld(PoseStack matrices, SubmitNodeCollector consumers, float tickDelta) {} + + @Override + public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); + if (!isEnabled() || mc.level == null) { TargetManager.setPredictionTarget(null); return; } + List results = PredictionsProjectileLogic.simulateProjectiles(this, tickDelta); + PredictionsTargetLogic.updatePredictionTarget(this, tickDelta); + int baseColor = lineColorValue.getArgb(); + if (aimTrajectoryValue.get()) PredictionsWorldRenderer.renderAimTrajectoryGlow(this, renderer, tickDelta, baseColor); + if (aimIndicatorValue.get()) PredictionsWorldRenderer.renderAimIndicatorWorld(this, renderer, tickDelta); + if (results.isEmpty()) return; + PredictionsWorldRenderer.renderLineTrajectories(this, renderer, results, baseColor); + PredictionsWorldRenderer.renderImpactEffects(this, renderer, results, baseColor, tickDelta); + } + + @Override + public void onRenderHudEngine(Renderer2D renderer, TextRenderer textRenderer, net.minecraft.client.gui.GuiGraphicsExtractor ctx, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); + if (!isEnabled() || mc.level == null) return; + boolean showPlate = showTimerPlateValue.get(); + timerPlates.clear(); + List results = showPlate ? PredictionsProjectileLogic.simulateProjectiles(this, tickDelta) : Collections.emptyList(); + if (showPlate && !results.isEmpty()) { + for (TrajectoryResult res : results) { + if (res.points().isEmpty()) continue; + Vec3 end = res.points().getLast(); + Vec3 screen = ScreenProjection.worldToScreen(end, tickDelta); + if (screen == null) continue; + PredictionsHudRenderer.drawTimerPlateForResult(this, renderer, textRenderer, res, (float) screen.x, (float) screen.y); + } + } + if (tntTimerValue.get()) PredictionsHudRenderer.drawTntTimers(this, renderer, textRenderer, tickDelta); + } + + @Override + public void onRenderHudEngineForeground(Renderer2D renderer, TextRenderer textRenderer, net.minecraft.client.gui.GuiGraphicsExtractor ctx, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); + if (!isEnabled() || mc.level == null || timerPlates.isEmpty()) return; + int seed = 0; + List plates = new ArrayList<>(timerPlates); + for (TimerPlate plate : plates) { + if (plate.iconStack().isEmpty()) continue; + renderer.item(plate.iconStack(), plate.iconX(), plate.iconY(), plate.iconScale(), seed++, Renderer2D.ITEM_OVERLAY_NONE, null); + } + if (!plates.isEmpty()) { + TextRenderer tr = Fonts.renderer("OnestMedium", FontInfo.Type.Regular, textRenderer); + tr.begin(0.68f); + for (TimerPlate plate : plates) tr.render(plate.text(), plate.textX(), plate.textY(), new RenderColor(0xFFFFFFFF), false); + tr.end(); + } + } + + public NumberValue getMaxSimTicks() { return maxSimTicks; } + public NumberValue getPearlMaxSimTicks() { return pearlMaxSimTicks; } + public NumberValue getMaxProjectiles() { return maxProjectiles; } + public NumberValue getPearlMaxProjectiles() { return pearlMaxProjectiles; } + public BooleanValue getPearlColorToggle() { return pearlColorToggle; } + public RGBColorValue getPearlColorValue() { return pearlColorValue; } + public BooleanMapValue getKindToggles() { return kindToggles; } + public BooleanValue getGlowWallsValue() { return glowWallsValue; } + public RGBColorValue getLineColorValue() { return lineColorValue; } + public BooleanValue getTargetHudOnAimValue() { return targetHudOnAimValue; } + public RGBColorValue getTntColorValue() { return tntColorValue; } + public List getTimerPlates() { return timerPlates; } + public ItemStack getTntTimerIcon() { + if (tntTimerIcon.isEmpty()) tntTimerIcon = new ItemStack(Items.TNT); + return tntTimerIcon; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/ProjectilePuncher.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/ProjectilePuncher.java new file mode 100644 index 0000000..d32a53c --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/ProjectilePuncher.java @@ -0,0 +1,104 @@ +package dev.loki.lovisual.features.module.modules.combat.prediction; + +import dev.loki.lovisual.config.common.CommonSettingSchemas; +import dev.loki.lovisual.config.values.mode.EnumValue; +import dev.loki.lovisual.config.values.primitive.BooleanMapValue; +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import dev.loki.lovisual.events.impl.input.RotationUpdateEvent; +import dev.loki.lovisual.events.meta.EventHandler; +import dev.loki.lovisual.features.module.core.Module; +import dev.loki.lovisual.features.module.core.ModuleCategory; +import dev.loki.lovisual.features.module.core.ModuleInfo; +import dev.loki.lovisual.util.aiming.data.Rotation; +import dev.loki.lovisual.util.aiming.features.MovementCorrection; +import dev.loki.lovisual.util.click.ClickScheduler; +import net.minecraft.client.Minecraft; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.Vec3; + +import java.util.List; + +@ModuleInfo(id = "projectilepuncher", displayName = "ProjectilePuncher", category = ModuleCategory.COMBAT) +public final class ProjectilePuncher extends Module { + public enum RaycastMode { STRICT, THROUGH_WALLS, NONE } + public enum RotationTiming { NORMAL, SNAP, ON_TICK } + public enum AngleSmoothMode { LINEAR, SIGMOID, SMART, SPOOKY, NONE } + static final int ROTATION_PRIORITY = 10; + final NumberValue range = numCommon("projectilePuncherRange", "range", CommonSettingSchemas.COMBAT_RANGE, 3.0, 2.0, 6.0); + final NumberValue wallRange = numCommon("projectilePuncherWallRange", "wall_range", CommonSettingSchemas.COMBAT_WALL_RANGE, 0.0, 0.0, 6.0); + final BooleanMapValue projectiles = common(group("projectilePuncherProjectiles", "projectiles", PuncherTargets.projectileDefaults()), CommonSettingSchemas.ITEMS_PROJECTILES.commonI18nKey()); + final BooleanValue ignoreOpenInventory = boolCommon("projectilePuncherIgnoreOpenInventory", "ignore_open_inventory", CommonSettingSchemas.INTERACTION_IGNORE_OPEN_INVENTORY, true); + final EnumValue raycast = enumCommon("projectilePuncherRaycast", "raycast", CommonSettingSchemas.COMBAT_RAYCAST, RaycastMode.STRICT, RaycastMode.values()); + final EnumValue rotationTiming = enumCommon("projectilePuncherRotationTiming", "rotation_timing", CommonSettingSchemas.ROTATION_TIMING, RotationTiming.NORMAL, RotationTiming.values()); + final EnumValue movementCorrection = enumCommon("projectilePuncherMovementCorrection", "movement_correction", CommonSettingSchemas.ROTATION_MOVEMENT_CORRECTION, MovementCorrection.STRICT, MovementCorrection.values()); + final EnumValue rotationMode = enumCommon("projectilePuncherRotationMode", "rotation_mode", CommonSettingSchemas.ROTATION_MODE, AngleSmoothMode.SMART, AngleSmoothMode.values()); + final NumberValue rotationResetTicks = numCommon("projectilePuncherRotationResetTicks", "rotation_reset_ticks", CommonSettingSchemas.ROTATION_RESET_TICKS, 2, 1, 20); + final NumberValue rotationResetThreshold = numCommon("projectilePuncherRotationResetThreshold", "rotation_reset_threshold", CommonSettingSchemas.ROTATION_RESET_THRESHOLD, 2.0f, 0.1f, 15.0f); + final BooleanValue rotationThroughWalls = boolCommon("projectilePuncherRotationThroughWalls", "rotation_through_walls", CommonSettingSchemas.ROTATION_THROUGH_WALLS, false); + final NumberValue predictTicks = num("projectilePuncherPredictTicks", "predict_ticks", 1, 0, 5); + final NumberValue cpsMin = numCommon("projectilePuncherCpsMin", "cps_min", CommonSettingSchemas.COMBAT_CPS_MIN, 20, 1, 20); + final NumberValue cpsMax = numCommon("projectilePuncherCpsMax", "cps_max", CommonSettingSchemas.COMBAT_CPS_MAX, 20, 1, 20); + final ClickScheduler clicker = new ClickScheduler(cpsMin.get(), cpsMax.get()); + final NumberValue linearHorMin = numCommon("projectilePuncherLinearHorizontalMin", "linear_horizontal_min", CommonSettingSchemas.ANGLESMOOTH_HORIZONTAL_MIN, 180f, 0f, 180f); + final NumberValue linearHorMax = numCommon("projectilePuncherLinearHorizontalMax", "linear_horizontal_max", CommonSettingSchemas.ANGLESMOOTH_HORIZONTAL_MAX, 180f, 0f, 180f); + final NumberValue linearVertMin = numCommon("projectilePuncherLinearVerticalMin", "linear_vertical_min", CommonSettingSchemas.ANGLESMOOTH_VERTICAL_MIN, 180f, 0f, 180f); + final NumberValue linearVertMax = numCommon("projectilePuncherLinearVerticalMax", "linear_vertical_max", CommonSettingSchemas.ANGLESMOOTH_VERTICAL_MAX, 180f, 0f, 180f); + final NumberValue sigmoidHorMin = numCommon("projectilePuncherSigmoidHorizontalMin", "sigmoid_horizontal_min", CommonSettingSchemas.ANGLESMOOTH_HORIZONTAL_MIN, 180f, 0f, 180f); + final NumberValue sigmoidHorMax = numCommon("projectilePuncherSigmoidHorizontalMax", "sigmoid_horizontal_max", CommonSettingSchemas.ANGLESMOOTH_HORIZONTAL_MAX, 180f, 0f, 180f); + final NumberValue sigmoidVertMin = numCommon("projectilePuncherSigmoidVerticalMin", "sigmoid_vertical_min", CommonSettingSchemas.ANGLESMOOTH_VERTICAL_MIN, 180f, 0f, 180f); + final NumberValue sigmoidVertMax = numCommon("projectilePuncherSigmoidVerticalMax", "sigmoid_vertical_max", CommonSettingSchemas.ANGLESMOOTH_VERTICAL_MAX, 180f, 0f, 180f); + final NumberValue sigmoidSteepness = numCommon("projectilePuncherSigmoidSteepness", "sigmoid_steepness", CommonSettingSchemas.ANGLESMOOTH_STEEPNESS, 10f, 0f, 20f); + final NumberValue sigmoidMidpoint = numCommon("projectilePuncherSigmoidMidpoint", "sigmoid_midpoint", CommonSettingSchemas.ANGLESMOOTH_MIDPOINT, 0.3f, 0f, 1f); + final NumberValue smartYawMin = numCommon("projectilePuncherSmartYawMin", "smart_yaw_min", CommonSettingSchemas.ANGLESMOOTH_YAW_MIN, 70f, 1f, 180f); + final NumberValue smartYawMax = numCommon("projectilePuncherSmartYawMax", "smart_yaw_max", CommonSettingSchemas.ANGLESMOOTH_YAW_MAX, 95f, 1f, 180f); + final NumberValue smartPitchMin = numCommon("projectilePuncherSmartPitchMin", "smart_pitch_min", CommonSettingSchemas.ANGLESMOOTH_PITCH_MIN, 45f, 1f, 180f); + final NumberValue smartPitchMax = numCommon("projectilePuncherSmartPitchMax", "smart_pitch_max", CommonSettingSchemas.ANGLESMOOTH_PITCH_MAX, 70f, 1f, 180f); + final NumberValue smartSnapThreshold = numCommon("projectilePuncherSmartSnapThreshold", "smart_snap_threshold", CommonSettingSchemas.ANGLESMOOTH_SNAP_THRESHOLD, 1.2f, 0.05f, 10.0f); + final NumberValue smartJitterYaw = numCommon("projectilePuncherSmartJitterYaw", "smart_jitter_yaw", CommonSettingSchemas.ANGLESMOOTH_JITTER_YAW, 0.8f, 0.0f, 6.0f); + final NumberValue smartJitterPitch = numCommon("projectilePuncherSmartJitterPitch", "smart_jitter_pitch", CommonSettingSchemas.ANGLESMOOTH_JITTER_PITCH, 0.4f, 0.0f, 4.0f); + final BooleanValue smartDecelerateEnabled = boolCommon("projectilePuncherSmartDecelerateEnabled", "smart_decelerate_enabled", CommonSettingSchemas.ANGLESMOOTH_DECELERATE_ENABLED, true); + final NumberValue smartDecelerateAngle = numCommon("projectilePuncherSmartDecelerateAngle", "smart_decelerate_angle", CommonSettingSchemas.ANGLESMOOTH_DECELERATE_ANGLE, 35.0f, 1.0f, 120.0f); + final NumberValue smartDecelerateMinFactor = numCommon("projectilePuncherSmartDecelerateMinFactor", "smart_decelerate_min_factor", CommonSettingSchemas.ANGLESMOOTH_DECELERATE_MIN_FACTOR, 0.22f, 0.05f, 1.0f); + Entity currentTarget; + Vec3 lastAimPoint; + int lastAimEntityId = -1; + Rotation lastRotation; + int lastRotationTick = -1; + int lastAttackExecutionAge = -1; + boolean rotationReleased = true; + final PuncherTargets targets = new PuncherTargets(this); + final PuncherRotations rotations = new PuncherRotations(this, targets); + final PuncherAttacks attacks = new PuncherAttacks(this, rotations); + + @Override public void onEnable() { + clicker.reset(); + currentTarget = null; + lastRotation = null; + lastRotationTick = -1; + lastAttackExecutionAge = -1; + rotationReleased = true; + } + + @Override public void onDisable() { attacks.clearTargetImmediate(); } + + @Override public void onTick() { + Minecraft mc = Minecraft.getInstance(); + if (mc.player == null || mc.level == null) attacks.clearTargetImmediate(); + } + + @EventHandler(priority = ROTATION_PRIORITY) + public void onRotationUpdate(RotationUpdateEvent event) { + Minecraft mc = Minecraft.getInstance(); + if (!isEnabled()) return; + if (mc.player == null || mc.level == null) { attacks.clearTargetImmediate(); return; } + if (event.getType() == RotationUpdateEvent.Type.POST) { attacks.tryAttackPost(); return; } + clicker.setCps(cpsMin.get(), cpsMax.get()); + clicker.tick(); + List candidates = targets.findCandidates(); + for (Entity entity : candidates) { + if (attacks.processTarget(entity, range.get().floatValue(), wallRange.get().floatValue())) { currentTarget = entity; return; } + } + attacks.clearTarget(); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/TargetESP.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/TargetESP.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/TargetESP.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/TargetESP.java diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/logic/PredictionsProjectileLogic.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/logic/PredictionsProjectileLogic.java new file mode 100644 index 0000000..380c80d --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/logic/PredictionsProjectileLogic.java @@ -0,0 +1,139 @@ +package dev.loki.lovisual.features.module.modules.combat.prediction; + +import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsAim; +import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsModel.AimPrediction; +import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsModel.Kind; +import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsModel.TrajectoryResult; +import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsSimulation; +import dev.loki.lovisual.mixins.accessors.projectile.PersistentProjectileEntityAccessor; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.entity.projectile.arrow.AbstractArrow; +import net.minecraft.world.entity.projectile.arrow.ThrownTrident; +import net.minecraft.world.entity.projectile.hurtingprojectile.Fireball; +import net.minecraft.world.entity.projectile.hurtingprojectile.windcharge.WindCharge; +import net.minecraft.world.entity.projectile.FireworkRocketEntity; +import net.minecraft.world.entity.projectile.throwableitemprojectile.AbstractThrownPotion; +import net.minecraft.world.entity.projectile.throwableitemprojectile.Snowball; +import net.minecraft.world.entity.projectile.throwableitemprojectile.ThrownEgg; +import net.minecraft.world.entity.projectile.throwableitemprojectile.ThrownEnderpearl; +import net.minecraft.world.item.Items; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public final class PredictionsProjectileLogic { + private PredictionsProjectileLogic() {} + + public static TrajectoryResult simulateEntity(Predictions facade, Entity e, Kind kind, float tickDelta) { + return PredictionsSimulation.simulateEntity(e, kind, tickDelta, + facade.getMaxSimTicks(), facade.getPearlMaxSimTicks(), + facade.getPearlColorToggle(), facade.getPearlColorValue()); + } + + public static AimPrediction computeAimPrediction(Predictions facade, Player player, float tickDelta) { + return PredictionsAim.computeAimPrediction(player, tickDelta, + facade.getMaxSimTicks(), facade.getPearlMaxSimTicks(), + facade.getPearlColorToggle(), facade.getPearlColorValue()); + } + + public static List simulateProjectiles(Predictions facade, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); + ClientLevel world = mc.level; + if (world == null) return Collections.emptyList(); + List out = new ArrayList<>(); + List candidates = new ArrayList<>(); + for (Entity e : world.entitiesForRendering()) candidates.add(e); + candidates.sort((a, b) -> { + double da = mc.player == null ? 0.0 : a.distanceToSqr(mc.player); + double db = mc.player == null ? 0.0 : b.distanceToSqr(mc.player); + return Double.compare(da, db); + }); + int limit = Math.max(1, facade.getMaxProjectiles().get()); + int processed = 0; + int pearlProcessed = 0; + for (Entity e : candidates) { + if (!e.isAlive() || !(e instanceof ThrownEnderpearl)) continue; + if (pearlProcessed >= facade.getPearlMaxProjectiles().get()) break; + TrajectoryResult res = simulateEntity(facade, e, Kind.PEARL_ENTITY, tickDelta); + if (res != null) out.add(res); + pearlProcessed++; + } + for (Entity e : candidates) { + if (!e.isAlive() || e instanceof ThrownEnderpearl) continue; + if (processed >= limit) break; + switch (e) { + case ThrownTrident trident -> { + if (isKindDisabled(facade, "trident") || trident.isNoPhysics() || isInGround(trident)) continue; + TrajectoryResult res = simulateEntity(facade, trident, Kind.TRIDENT, tickDelta); + if (res != null) out.add(res); + processed++; + } + case AbstractArrow arrow -> { + if (isKindDisabled(facade, "arrow") || arrow.isNoPhysics() || isInGround(arrow)) continue; + TrajectoryResult res = simulateEntity(facade, arrow, Kind.ARROW, tickDelta); + if (res != null) out.add(res); + processed++; + } + case Fireball fireball -> { + if (isKindDisabled(facade, "fireball")) continue; + TrajectoryResult res = simulateEntity(facade, fireball, Kind.FIREBALL, tickDelta); + if (res != null) out.add(res); + processed++; + } + case WindCharge windCharge -> { + if (isKindDisabled(facade, "wind_charge")) continue; + TrajectoryResult res = simulateEntity(facade, windCharge, Kind.WIND_CHARGE, tickDelta); + if (res != null) out.add(res); + processed++; + } + case FireworkRocketEntity firework -> { + if (isKindDisabled(facade, "firework")) continue; + TrajectoryResult res = simulateEntity(facade, firework, Kind.FIREWORK, tickDelta); + if (res != null) out.add(res); + processed++; + } + case Snowball snowball -> { + if (isKindDisabled(facade, "snowball")) continue; + TrajectoryResult res = simulateEntity(facade, snowball, Kind.SNOWBALL, tickDelta); + if (res != null) out.add(res); + processed++; + } + case ThrownEgg egg -> { + if (isKindDisabled(facade, "egg")) continue; + TrajectoryResult res = simulateEntity(facade, egg, Kind.EGG, tickDelta); + if (res != null) out.add(res); + processed++; + } + case AbstractThrownPotion potion -> { + boolean lingering = potion.getItem().is(Items.LINGERING_POTION); + boolean splash = potion.getItem().is(Items.SPLASH_POTION) || !lingering; + if (lingering && isKindDisabled(facade, "potion_lingering")) continue; + if (splash && isKindDisabled(facade, "potion_splash")) continue; + Kind kind = lingering ? Kind.POTION_LINGERING : Kind.POTION_SPLASH; + TrajectoryResult res = simulateEntity(facade, potion, kind, tickDelta); + if (res != null) out.add(res); + processed++; + } + default -> {} + } + } + return out; + } + + private static boolean isKindDisabled(Predictions facade, String key) { + return !facade.getKindToggles().get(key); + } + + private static boolean isInGround(AbstractArrow arrow) { + if (arrow == null) return false; + try { + return ((PersistentProjectileEntityAccessor) arrow).lovisual$isInGround(); + } catch (Throwable t) { + return false; + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/logic/PredictionsTargetLogic.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/logic/PredictionsTargetLogic.java new file mode 100644 index 0000000..fff1474 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/logic/PredictionsTargetLogic.java @@ -0,0 +1,30 @@ +package dev.loki.lovisual.features.module.modules.combat.prediction; + +import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsModel.AimPrediction; +import dev.loki.lovisual.util.target.TargetManager; +import net.minecraft.client.Minecraft; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; + +public final class PredictionsTargetLogic { + private PredictionsTargetLogic() {} + + public static void updatePredictionTarget(Predictions facade, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); + if (!facade.getTargetHudOnAimValue().get()) { + TargetManager.setPredictionTarget(null); + return; + } + Player player = mc.player; + if (player == null || mc.level == null) { + TargetManager.setPredictionTarget(null); + return; + } + AimPrediction pred = PredictionsProjectileLogic.computeAimPrediction(facade, player, tickDelta); + if (pred != null && pred.entity() instanceof LivingEntity living && living.isAlive() && !living.isRemoved()) { + TargetManager.setPredictionTarget(living); + return; + } + TargetManager.setPredictionTarget(null); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/puncher/PuncherAttacks.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/puncher/PuncherAttacks.java new file mode 100644 index 0000000..897d647 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/puncher/PuncherAttacks.java @@ -0,0 +1,79 @@ +package dev.loki.lovisual.features.module.modules.combat.prediction; + +import dev.loki.lovisual.util.aiming.data.Rotation; +import dev.loki.lovisual.util.aiming.data.RotationWithVector; +import dev.loki.lovisual.util.aiming.rotation.RotationManager; +import dev.loki.lovisual.util.aiming.rotation.RotationTarget; +import net.minecraft.client.Minecraft; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.Entity; + +final class PuncherAttacks { + private final ProjectilePuncher module; + private final PuncherRotations rotations; + + PuncherAttacks(ProjectilePuncher module, PuncherRotations rotations) { + this.module = module; + this.rotations = rotations; + } + + boolean processTarget(Entity target, float range, float wallsRange) { + Minecraft mc = Minecraft.getInstance(); + RotationWithVector rotation = rotations.findRotation(target, range, wallsRange); + if (rotation == null) return false; + module.lastRotation = rotation.rotation(); + module.lastRotationTick = mc.player != null ? mc.player.tickCount : -1; + module.rotationReleased = false; + ProjectilePuncher.RotationTiming timing = module.rotationTiming.get(); + int rotTicks = rotations.calculateTicks(rotation.rotation()); + if (timing == ProjectilePuncher.RotationTiming.SNAP && !module.clicker.willClickAt(Math.max(1, rotTicks))) return true; + RotationTarget plan = rotations.buildRotationTarget(rotation.rotation(), target); + RotationManager.INSTANCE.setRotationTarget(plan, ProjectilePuncher.ROTATION_PRIORITY, module); + return true; + } + + void tryAttackPost() { + Minecraft mc = Minecraft.getInstance(); + if (mc.player == null || mc.level == null || mc.gameMode == null) return; + if (mc.player.tickCount == module.lastAttackExecutionAge) return; + Entity target = module.currentTarget; + if (target == null || target.isRemoved()) return; + if (!rotations.isInRange(target)) return; + Rotation rotation = rotations.resolveAttackRotation(target); + if (rotation == null) { + clearTarget(); + return; + } + if (!rotations.passesRaycast(target, rotation.yaw(), rotation.pitch())) return; + int clickCount = module.clicker.getClicksAt(0); + if (clickCount <= 0) return; + module.lastAttackExecutionAge = mc.player.tickCount; + for (int i = 0; i < clickCount; i++) { + if (target.isRemoved()) break; + mc.gameMode.attack(mc.player, target); + mc.player.swing(InteractionHand.MAIN_HAND); + } + } + + void clearTarget() { + module.currentTarget = null; + module.lastAttackExecutionAge = -1; + clearRotationState(true); + } + + void clearTargetImmediate() { + module.currentTarget = null; + module.lastAttackExecutionAge = -1; + clearRotationState(false); + } + + void clearRotationState(boolean smoothReturn) { + if (smoothReturn && module.rotationReleased) return; + RotationManager.INSTANCE.release(module, smoothReturn); + module.lastAimPoint = null; + module.lastAimEntityId = -1; + module.lastRotation = null; + module.lastRotationTick = -1; + module.rotationReleased = true; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/puncher/PuncherRotations.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/puncher/PuncherRotations.java new file mode 100644 index 0000000..0b06e2f --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/puncher/PuncherRotations.java @@ -0,0 +1,134 @@ +package dev.loki.lovisual.features.module.modules.combat.prediction; + +import dev.loki.lovisual.util.aiming.data.Rotation; +import dev.loki.lovisual.util.aiming.data.RotationWithVector; +import dev.loki.lovisual.util.aiming.features.MovementCorrection; +import dev.loki.lovisual.util.aiming.features.processors.RotationProcessor; +import dev.loki.lovisual.util.aiming.features.processors.anglesmooth.AngleSmooth; +import dev.loki.lovisual.util.aiming.features.processors.anglesmooth.impl.LinearAngleSmooth; +import dev.loki.lovisual.util.aiming.features.processors.anglesmooth.impl.curves.SigmoidAngleSmooth; +import dev.loki.lovisual.util.aiming.features.processors.anglesmooth.impl.curves.SmartAngleSmooth; +import dev.loki.lovisual.util.aiming.features.processors.anglesmooth.impl.curves.SpookyAngleSmooth; +import dev.loki.lovisual.util.aiming.preference.LeastDifferencePreference; +import dev.loki.lovisual.util.aiming.preference.RotationPreference; +import dev.loki.lovisual.util.aiming.raytrace.RotationRaytrace; +import dev.loki.lovisual.util.aiming.rotation.RotationManager; +import dev.loki.lovisual.util.aiming.rotation.RotationTarget; +import dev.loki.lovisual.util.raycast.RaycastUtil; +import dev.loki.lovisual.util.target.TargetingUtil; +import net.minecraft.client.Minecraft; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; + +import java.util.ArrayList; +import java.util.List; + +final class PuncherRotations { + private final ProjectilePuncher module; + private final PuncherTargets targets; + private final LinearAngleSmooth linearSmooth; + private final SigmoidAngleSmooth sigmoidSmooth; + private final SmartAngleSmooth smartSmooth; + private final SpookyAngleSmooth spookySmooth = new SpookyAngleSmooth(); + + PuncherRotations(ProjectilePuncher module, PuncherTargets targets) { + this.module = module; + this.targets = targets; + this.linearSmooth = new LinearAngleSmooth(module.linearHorMin, module.linearHorMax, module.linearVertMin, module.linearVertMax); + this.sigmoidSmooth = new SigmoidAngleSmooth(module.sigmoidHorMin, module.sigmoidHorMax, module.sigmoidVertMin, module.sigmoidVertMax, module.sigmoidSteepness, module.sigmoidMidpoint); + this.smartSmooth = new SmartAngleSmooth(module.smartYawMin, module.smartYawMax, module.smartPitchMin, module.smartPitchMax, module.smartSnapThreshold, module.smartJitterYaw, module.smartJitterPitch, module.smartDecelerateEnabled, module.smartDecelerateAngle, module.smartDecelerateMinFactor); + } + + RotationWithVector findRotation(Entity target, float range, float wallsRange) { + Minecraft mc = Minecraft.getInstance(); + if (mc.player == null) return null; + Vec3 eyes = mc.player.getEyePosition(); + AABB targetBox = targets.predictBox(target); + RotationPreference pref = module.lastAimEntityId == target.getId() && module.lastAimPoint != null ? LeastDifferencePreference.leastDifferenceToLastPoint(eyes, module.lastAimPoint) : LeastDifferencePreference.LEAST_DISTANCE_TO_CURRENT_ROTATION; + RotationWithVector rot = RotationRaytrace.raytraceBox(eyes, targetBox, range, wallsRange, RotationRaytrace.OUTLINE_VISIBILITY, pref, true); + if (rot == null && module.rotationThroughWalls.get()) rot = RotationRaytrace.raytraceBox(eyes, targetBox, range, range, RotationRaytrace.OUTLINE_VISIBILITY, pref, true); + if (rot != null) { + rot = stabilizeRotationResult(target, eyes, targetBox, rot); + module.lastAimPoint = rot.vec(); + module.lastAimEntityId = target.getId(); + } + return rot; + } + + RotationWithVector stabilizeRotationResult(Entity target, Vec3 eyes, AABB box, RotationWithVector rotation) { + if (target == null || eyes == null || box == null || rotation == null) return rotation; + Vec3 stablePoint = rotation.vec(); + if (module.lastAimEntityId == target.getId() && module.lastAimPoint != null) { + Vec3 previousPoint = clampToBox(module.lastAimPoint, box); + double distance = previousPoint.distanceTo(stablePoint); + double blend = distance > 0.50 ? 0.55 : distance > 0.20 ? 0.35 : 0.20; + stablePoint = new Vec3(Mth.lerp(blend, previousPoint.x, stablePoint.x), Mth.lerp(blend, previousPoint.y, stablePoint.y), Mth.lerp(blend, previousPoint.z, stablePoint.z)); + stablePoint = clampToBox(stablePoint, box); + } + Rotation stableRotation = Rotation.lookingAt(stablePoint, eyes).normalize(); + return new RotationWithVector(stableRotation, stablePoint); + } + + Vec3 clampToBox(Vec3 point, AABB box) { + return new Vec3(Mth.clamp(point.x, box.minX, box.maxX), Mth.clamp(point.y, box.minY, box.maxY), Mth.clamp(point.z, box.minZ, box.maxZ)); + } + + boolean passesRaycast(Entity target, float yaw, float pitch) { + Minecraft mc = Minecraft.getInstance(); + ProjectilePuncher.RaycastMode mode = module.raycast.get(); + if (mode == ProjectilePuncher.RaycastMode.NONE) return true; + double maxRange = module.range.get(); + if (mode == ProjectilePuncher.RaycastMode.THROUGH_WALLS) return RaycastUtil.isLookingAtEntity(mc.player, target, yaw, pitch, maxRange, module.wallRange.get()) != null; + return RaycastUtil.isLookingAtEntity(mc.player, target, yaw, pitch, maxRange, 0.0) != null; + } + + boolean isInRange(Entity target) { + Minecraft mc = Minecraft.getInstance(); + return TargetingUtil.distanceToEntityBoxSq(mc.player.getEyePosition(), target) <= module.range.get() * module.range.get(); + } + + Rotation resolveAttackRotation(Entity target) { + Minecraft mc = Minecraft.getInstance(); + if (mc.player == null) return null; + ProjectilePuncher.RotationTiming timing = module.rotationTiming.get(); + if (timing == ProjectilePuncher.RotationTiming.ON_TICK) { + Rotation rotation = module.lastRotationTick == mc.player.tickCount ? module.lastRotation : null; + if (rotation == null) { + RotationWithVector rot = findRotation(target, module.range.get().floatValue(), module.wallRange.get().floatValue()); + rotation = rot != null ? rot.rotation() : null; + if (rotation == null) return null; + } + return rotation.normalize(); + } + Rotation current = RotationManager.INSTANCE.getCurrentRotation(); + Rotation fallback = current != null ? current : new Rotation(mc.player.getYRot(), mc.player.getXRot(), true); + return fallback.normalize(); + } + + RotationTarget buildRotationTarget(Rotation rotation, Entity target) { + List processors = new ArrayList<>(); + AngleSmooth smooth = selectAngleSmooth(); + if (smooth != null) processors.add(smooth); + return new RotationTarget(rotation, target, processors, module.rotationResetTicks.get(), module.rotationResetThreshold.get(), !module.ignoreOpenInventory.get(), module.movementCorrection.get(), null); + } + + int calculateTicks(Rotation rotation) { + AngleSmooth smooth = selectAngleSmooth(); + if (smooth == null) return 0; + Rotation base = RotationManager.INSTANCE.getServerRotation(); + if (base == null) return 0; + return smooth.calculateTicks(base, rotation); + } + + AngleSmooth selectAngleSmooth() { + return switch (module.rotationMode.get()) { + case LINEAR -> linearSmooth; + case SIGMOID -> sigmoidSmooth; + case SMART -> smartSmooth; + case SPOOKY -> spookySmooth; + case NONE -> null; + }; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/puncher/PuncherTargets.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/puncher/PuncherTargets.java new file mode 100644 index 0000000..a557a9d --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/puncher/PuncherTargets.java @@ -0,0 +1,71 @@ +package dev.loki.lovisual.features.module.modules.combat.prediction; + +import net.minecraft.client.Minecraft; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import dev.loki.lovisual.util.target.TargetingUtil; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +final class PuncherTargets { + static final String PROJECTILE_FIREBALL = "fireball"; + static final String PROJECTILE_SHULKER_BULLET = "shulker_bullet"; + private final ProjectilePuncher module; + + PuncherTargets(ProjectilePuncher module) { + this.module = module; + } + + static Map projectileDefaults() { + return Map.of(PROJECTILE_FIREBALL, true, PROJECTILE_SHULKER_BULLET, true); + } + + List findCandidates() { + Minecraft mc = Minecraft.getInstance(); + if (mc.player == null || mc.level == null) return List.of(); + double searchRange = Math.max(module.range.get(), module.wallRange.get()) + 8.0; + List candidates = mc.level.getEntities(mc.player, mc.player.getBoundingBox().inflate(searchRange), this::isSupportedProjectile); + candidates.removeIf(e -> !shouldAttack(e)); + candidates.sort(Comparator.comparingDouble(this::predictedDistanceSq)); + return candidates; + } + + boolean shouldAttack(Entity entity) { + Minecraft mc = Minecraft.getInstance(); + if (mc.player == null) return false; + Vec3 velocity = entity.getDeltaMovement(); + if (velocity.lengthSqr() < 1.0E-7) return false; + Vec3 entityPos = entity.position(); + Vec3 vecToPlayer = mc.player.getBoundingBox().getCenter().subtract(entityPos); + double dot = vecToPlayer.dot(velocity); + boolean movingTowardsPlayer = dot > 0.5 * velocity.length() * vecToPlayer.length(); + AABB extendedHitbox = mc.player.getBoundingBox().inflate(entity.getBoundingBox().getXsize() * 0.5); + boolean touchesHitbox = extendedHitbox.clip(entityPos, entityPos.add(velocity.scale(20.0))).isPresent(); + boolean willHitPlayer = !extendedHitbox.contains(entityPos) && touchesHitbox; + if (!movingTowardsPlayer && !willHitPlayer) return false; + return predictedDistanceSq(entity) <= module.range.get() * module.range.get(); + } + + boolean isSupportedProjectile(Entity entity) { + if (entity == null || entity.isRemoved()) return false; + EntityType type = entity.getType(); + if (type == net.minecraft.world.entity.EntityTypes.FIREBALL) return module.projectiles.get(PROJECTILE_FIREBALL); + if (type == net.minecraft.world.entity.EntityTypes.SHULKER_BULLET) return module.projectiles.get(PROJECTILE_SHULKER_BULLET); + return false; + } + + AABB predictBox(Entity entity) { + int ticks = Math.max(0, module.predictTicks.get()); + if (ticks <= 0) return entity.getBoundingBox(); + return entity.getBoundingBox().move(entity.getDeltaMovement().scale(ticks)); + } + + double predictedDistanceSq(Entity entity) { + Minecraft mc = Minecraft.getInstance(); + return TargetingUtil.distanceToBoxSq(mc.player.getEyePosition(), predictBox(entity)); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/render/PredictionsHudRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/render/PredictionsHudRenderer.java new file mode 100644 index 0000000..626592e --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/render/PredictionsHudRenderer.java @@ -0,0 +1,131 @@ +package dev.loki.lovisual.features.module.modules.combat.prediction; + +import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsModel.Kind; +import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsModel.TimerPlate; +import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsModel.TrajectoryResult; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.font.info.FontInfo; +import dev.loki.lovisual.render.engine.text.font.Fonts; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.render.engine.color.RenderColor; +import dev.loki.lovisual.render.helpers.clip.ScreenProjection; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.item.PrimedTnt; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.phys.Vec3; + +import java.util.Locale; + +public final class PredictionsHudRenderer { + private PredictionsHudRenderer() {} + + private static final float PLATE_RADIUS = 1.7f; + private static final float PLATE_TEXT_SCALE = 0.68f; + private static final float PLATE_TEXT_PADDING = 24.0f; + private static final float PLATE_HEIGHT = 16.0f; + private static final float PLATE_BG_OFFSET_X = 2.0f; + private static final float PLATE_BG_OFFSET_Y = 0.0f; + private static final float PLATE_BG_SHRINK = 3.0f; + private static final float PLATE_ICON_SIZE = 13.0f; + private static final float PLATE_ICON_OFFSET_X = 4.0f; + private static final float PLATE_ICON_TEXT_GAP = 4.0f; + private static final float PLATE_TEXT_OFFSET_Y = 0.0f; + private static final int PLATE_SHADOW = 0xAA000000; + private static final float PLATE_SHADOW_BLUR = 3.6f; + private static final float PLATE_SHADOW_INNER_ALPHA = 0.18f; + + public static void drawTimerPlateForResult(Predictions facade, Renderer2D renderer, TextRenderer textRenderer, + TrajectoryResult res, float sx, float sy) { + double seconds = res.flightTicks() / 20.0; + if (seconds <= 0.0) return; + ItemStack iconStack = useItemIcon(res.kind()) ? resolveTimerItem(res.kind()) : ItemStack.EMPTY; + Identifier iconId = iconStack.isEmpty() ? resolveTimerIcon(res.kind()) : null; + drawTimerPlate(facade, renderer, textRenderer, formatTime(seconds), sx, sy, res.argb(), iconStack, iconId); + } + + public static void drawTntTimers(Predictions facade, Renderer2D renderer, TextRenderer textRenderer, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); + ClientLevel world = mc.level; + if (world == null) return; + int plateColor = facade.getTntColorValue().getArgb(); + for (Entity entity : world.entitiesForRendering()) { + if (!(entity instanceof PrimedTnt tnt) || !tnt.isAlive() || tnt.isRemoved()) continue; + int fuse = tnt.getFuse(); + if (fuse < 0) continue; + Vec3 anchor = tnt.getPosition(tickDelta).add(0.0, tnt.getBbHeight() + 0.5, 0.0); + Vec3 screen = ScreenProjection.worldToScreen(anchor, tickDelta); + if (screen == null) continue; + double seconds = Math.max(0.0, (fuse - tickDelta) / 20.0); + drawTimerPlate(facade, renderer, textRenderer, formatTime(seconds), + (float) screen.x, (float) screen.y, plateColor, facade.getTntTimerIcon(), null); + } + } + + public static void drawTimerPlate(Predictions facade, Renderer2D renderer, TextRenderer textRenderer, + String txt, float sx, float sy, int argb, + ItemStack iconStack, Identifier iconId) { + if (txt == null || txt.isEmpty()) return; + TextRenderer tr = Fonts.renderer("OnestMedium", FontInfo.Type.Regular, textRenderer); + float textW = (float) tr.getWidth(txt) * PLATE_TEXT_SCALE; + float plateW = textW + PLATE_TEXT_PADDING; + float plateX = sx - plateW * 0.5f; + float plateY = sy; + float bgX = plateX + PLATE_BG_OFFSET_X; + float bgY = plateY + PLATE_BG_OFFSET_Y; + float bgW = plateW - PLATE_BG_SHRINK; + int plateShadow = argb != 0 ? (argb & 0x00FFFFFF) | (PLATE_SHADOW & 0xFF000000) : PLATE_SHADOW; + renderer.roundedRectSoftShadow(bgX, bgY, bgW, PLATE_HEIGHT, PLATE_RADIUS, PLATE_SHADOW_BLUR, PLATE_SHADOW_INNER_ALPHA, plateShadow); + float iconX = plateX + PLATE_ICON_OFFSET_X; + float iconY = plateY + (PLATE_HEIGHT - PLATE_ICON_SIZE) * 0.5f; + float iconScale = PLATE_ICON_SIZE / 16.0f; + if (iconStack == null || iconStack.isEmpty()) { + iconStack = ItemStack.EMPTY; + drawTimerIcon(iconId, iconX, iconY, PLATE_ICON_SIZE); + } + float textX = iconX + PLATE_ICON_SIZE + PLATE_ICON_TEXT_GAP; + float textH = (float) tr.getHeight() * PLATE_TEXT_SCALE; + float textY = plateY + (PLATE_HEIGHT - textH) * 0.5f + PLATE_TEXT_OFFSET_Y; + facade.getTimerPlates().add(new TimerPlate(textX, textY, txt, iconX, iconY, iconScale, iconStack)); + } + + private static void drawTimerIcon(Identifier iconId, float x, float y, float size) { + if (iconId == null) return; + Renderer2D.TEXTURE.roundedTexRect(x, y, size, size, 1.5f, 1.0f, 0, 0, 1, 1, 0xFFFFFFFF, iconId); + } + + private static boolean useItemIcon(Kind kind) { + return kind == Kind.SNOWBALL || kind == Kind.EGG || kind == Kind.FIREBALL + || kind == Kind.FIREWORK || kind == Kind.WIND_CHARGE; + } + + private static ItemStack resolveTimerItem(Kind kind) { + return switch (kind) { + case SNOWBALL -> new ItemStack(Items.SNOWBALL); + case EGG -> new ItemStack(Items.EGG); + case FIREBALL -> new ItemStack(Items.FIRE_CHARGE); + case FIREWORK -> new ItemStack(Items.FIREWORK_ROCKET); + case WIND_CHARGE -> new ItemStack(Items.WIND_CHARGE); + default -> ItemStack.EMPTY; + }; + } + + private static Identifier resolveTimerIcon(Kind kind) { + return switch (kind) { + case PEARL_ENTITY -> Identifier.fromNamespaceAndPath("lovisual", "textures/timers/pearl.png"); + case TRIDENT -> Identifier.fromNamespaceAndPath("lovisual", "textures/timers/trident.png"); + case POTION_SPLASH, POTION_LINGERING -> Identifier.fromNamespaceAndPath("lovisual", "textures/timers/potion.png"); + case ARROW -> Identifier.fromNamespaceAndPath("lovisual", "textures/timers/arrow.png"); + case FIREBALL, FIREWORK, WIND_CHARGE, SNOWBALL, EGG -> null; + }; + } + + public static String formatTime(double seconds) { + String s = String.format(Locale.US, "%.1f", seconds); + s = s.replace('.', ','); + return s + " s"; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/render/PredictionsWorldRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/render/PredictionsWorldRenderer.java new file mode 100644 index 0000000..8501538 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/render/PredictionsWorldRenderer.java @@ -0,0 +1,193 @@ +package dev.loki.lovisual.features.module.modules.combat.prediction; + +import com.mojang.blaze3d.pipeline.RenderPipeline; +import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsModel.AimPrediction; +import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsModel.TrajectoryResult; +import dev.loki.lovisual.features.module.modules.visuals.optimization.FpsOptimizer; +import dev.loki.lovisual.render.engine.RenderState; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; +import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import dev.loki.lovisual.render.engine.texture.TextureStorage; +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; +import net.minecraft.client.Minecraft; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import org.joml.Quaternionf; + +import java.util.List; + +import static dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsGeometry.*; +import static dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsSimulation.HIT_ENTITY_COLOR; + +public final class PredictionsWorldRenderer { + private PredictionsWorldRenderer() {} + + private static final int GLOW_SUBSTEPS = 10; + private static final float GLOW_CORE_SIZE = 0.085f; + private static final float GLOW_SOFT_SIZE = 0.16f; + private static final float GLOW_CORE_ALPHA = 0.74f; + private static final float GLOW_SOFT_ALPHA = 0.32f; + private static final int AIM_GLOW_SUBSTEPS = 7; + private static final float AIM_GLOW_CORE_SIZE = 0.055f; + private static final float AIM_GLOW_SOFT_SIZE = 0.105f; + private static final float AIM_GLOW_CORE_ALPHA = 0.42f; + private static final float AIM_GLOW_SOFT_ALPHA = 0.17f; + private static final double AIM_GLOW_MIN_CAMERA_DISTANCE_SQ = 0.56; + private static final float IMPACT_RING_RADIUS = 0.30f; + private static final float IMPACT_RING_LINE_WIDTH = 1.2f; + private static final float IMPACT_RING_ALPHA = 0.88f; + private static final float IMPACT_DECAL_RADIUS = 0.34f; + private static final int ENTITY_HIT_FILL_ALPHA = 34; + private static final int ENTITY_HIT_LINE_ALPHA = 170; + private static final float TRAJECTORY_LINE_WIDTH = 1.35f; + private static final float TRAJECTORY_LINE_ALPHA_START = 0.16f; + private static final float TRAJECTORY_LINE_ALPHA_END = 0.78f; + private static final double IMPACT_BOX_EXPAND = 0.03; + + public static void renderAimTrajectoryGlow(Predictions facade, Renderer3D renderer, float tickDelta, int baseColor) { + Minecraft mc = Minecraft.getInstance(); + Player player = mc.player; + if (player == null) return; + AimPrediction pred = PredictionsProjectileLogic.computeAimPrediction(facade, player, tickDelta); + if (pred == null || pred.trajectory() == null) return; + List points = pred.trajectory().points(); + if (points.size() < 2) return; + boolean walls = facade.getGlowWallsValue().get(); + RenderPipeline pipeline = walls ? LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE : LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE_LIQUID_IGNORE; + Renderer3D.DepthMode depthMode = walls ? Renderer3D.DepthMode.MAIN : Renderer3D.DepthMode.PRE_DEPTH; + if (FpsOptimizer.bloomDisabled()) return; + MeshBuilder mesh = renderer.batchTextured(pipeline, TextureStorage.BLOOM, depthMode); + if (mesh == null) return; + int color = pred.entity() != null ? HIT_ENTITY_COLOR : pred.trajectory().argb() != 0 ? pred.trajectory().argb() : baseColor; + Quaternionf camRot = RenderState.cameraRotation; + Vec3 cameraPos = RenderState.cameraPos != null ? RenderState.cameraPos : player.getEyePosition(tickDelta); + int totalSegments = points.size() - 1; + for (int i = 1; i < points.size(); i++) { + Vec3 from = points.get(i - 1); + Vec3 to = points.get(i); + for (int step = 0; step <= AIM_GLOW_SUBSTEPS; step++) { + float stepT = step / (float) AIM_GLOW_SUBSTEPS; + Vec3 pos = from.lerp(to, stepT); + if (pos.distanceToSqr(cameraPos) < AIM_GLOW_MIN_CAMERA_DISTANCE_SQ) continue; + float segmentProgress = ((i - 1) + stepT) / Math.max(1.0f, totalSegments); + float strength = 0.28f + 0.72f * AnimationUtility.easeOutCubic(segmentProgress); + addBillboardQuad(mesh, pos.x, pos.y, pos.z, AIM_GLOW_CORE_SIZE, camRot, multiplyAlpha(color, AIM_GLOW_CORE_ALPHA * strength)); + addBillboardQuad(mesh, pos.x, pos.y, pos.z, AIM_GLOW_SOFT_SIZE, camRot, multiplyAlpha(color, AIM_GLOW_SOFT_ALPHA * strength)); + } + } + } + + public static void renderAimIndicatorWorld(Predictions facade, Renderer3D renderer, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); + Player player = mc.player; + if (player == null) return; + AimPrediction pred = PredictionsProjectileLogic.computeAimPrediction(facade, player, tickDelta); + if (pred == null) return; + float prevWidth = RenderState.lineWidth; + RenderState.lineWidth = IMPACT_RING_LINE_WIDTH; + try { + MeshBuilder mesh = renderer.batch(LoVisualRenderPipelines.WORLD_COLORED_LINES_LIQUID_IGNORE, Renderer3D.DepthMode.NONE); + if (mesh == null) return; + int color = pred.entity() != null ? HIT_ENTITY_COLOR : facade.getLineColorValue().getArgb(); + float pulseTime = (mc.level.getGameTime() + tickDelta) * 0.085f; + float pulse = 0.9f + 0.1f * AnimationUtility.smoothstep((float) ((Math.sin(pulseTime) + 1.0) * 0.5)); + Vec3 normal = pred.hitNormal() != null ? pred.hitNormal() : new Vec3(0.0, 1.0, 0.0); + addImpactRing(mesh, pred.hitPos(), normal, IMPACT_RING_RADIUS * pulse, multiplyAlpha(color, IMPACT_RING_ALPHA)); + } finally { + RenderState.lineWidth = prevWidth; + } + } + + public static void renderLineTrajectories(Predictions facade, Renderer3D renderer, List results, int baseColor) { + boolean walls = facade.getGlowWallsValue().get(); + RenderPipeline pipeline = walls ? LoVisualRenderPipelines.WORLD_COLORED_LINES_LIQUID_BLEND : LoVisualRenderPipelines.WORLD_COLORED_LINES_LIQUID_IGNORE; + Renderer3D.DepthMode depthMode = walls ? Renderer3D.DepthMode.MAIN : Renderer3D.DepthMode.PRE_DEPTH; + float prevWidth = RenderState.lineWidth; + RenderState.lineWidth = TRAJECTORY_LINE_WIDTH; + try { + MeshBuilder mesh = renderer.batch(pipeline, depthMode); + if (mesh == null) return; + for (TrajectoryResult res : results) { + List points = res.points(); + if (points.size() < 2) continue; + int color = res.argb() != 0 ? res.argb() : baseColor; + int totalSegments = points.size() - 1; + for (int i = 1; i < points.size(); i++) { + Vec3 from = points.get(i - 1); + Vec3 to = points.get(i); + float startT = (i - 1) / (float) totalSegments; + float endT = i / (float) totalSegments; + float startAlpha = Mth.lerp(AnimationUtility.easeOutCubic(startT), TRAJECTORY_LINE_ALPHA_START, TRAJECTORY_LINE_ALPHA_END); + float endAlpha = Mth.lerp(AnimationUtility.easeOutCubic(endT), TRAJECTORY_LINE_ALPHA_START, TRAJECTORY_LINE_ALPHA_END); + addGradientLine(mesh, from, to, multiplyAlpha(color, startAlpha), multiplyAlpha(color, endAlpha)); + } + } + } finally { + RenderState.lineWidth = prevWidth; + } + } + + public static void renderImpactEffects(Predictions facade, Renderer3D renderer, List results, int baseColor, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); + boolean walls = facade.getGlowWallsValue().get(); + RenderPipeline linePipeline = walls ? LoVisualRenderPipelines.WORLD_COLORED_LINES_LIQUID_BLEND : LoVisualRenderPipelines.WORLD_COLORED_LINES_LIQUID_IGNORE; + Renderer3D.DepthMode lineDepthMode = walls ? Renderer3D.DepthMode.MAIN : Renderer3D.DepthMode.PRE_DEPTH; + RenderPipeline decalPipeline = walls ? LoVisualRenderPipelines.WORLD_DECAL_SDF : LoVisualRenderPipelines.WORLD_DECAL_SDF_DEPTH; + Renderer3D.DepthMode decalDepthMode = walls ? Renderer3D.DepthMode.NONE : Renderer3D.DepthMode.PRE_DEPTH; + float prevWidth = RenderState.lineWidth; + RenderState.lineWidth = IMPACT_RING_LINE_WIDTH; + try { + MeshBuilder decalMesh = renderer.batch(decalPipeline, decalDepthMode); + renderer.batch(linePipeline, lineDepthMode); + for (TrajectoryResult res : results) { + Vec3 impact = res.impactPos(); + if (impact != null && decalMesh != null) { + int color = res.argb() != 0 ? res.argb() : baseColor; + float pulseTime = (mc.level.getGameTime() + tickDelta) * 0.085f; + float pulse = 0.85f + 0.15f * AnimationUtility.smoothstep((float) ((Math.sin(pulseTime) + 1.0) * 0.5)); + Vec3 normal = res.impactNormal() != null ? res.impactNormal() : new Vec3(0.0, 1.0, 0.0); + addImpactDecal(decalMesh, impact, normal, IMPACT_DECAL_RADIUS * pulse, color); + } + if (res.hitEntity() != null && res.hitEntity().isAlive()) { + int color = res.argb() != 0 ? res.argb() : baseColor; + AABB box = res.hitEntity().getBoundingBox().inflate(IMPACT_BOX_EXPAND); + addFilledBox(renderer, box, withAlpha(color, ENTITY_HIT_FILL_ALPHA)); + addOutlineBox(renderer, box, withAlpha(color, ENTITY_HIT_LINE_ALPHA)); + } + } + } finally { + RenderState.lineWidth = prevWidth; + } + } + + private static void addFilledBox(Renderer3D renderer, AABB box, int argb) { + int a = (argb >>> 24) & 0xFF; int r = (argb >>> 16) & 0xFF; int g = (argb >>> 8) & 0xFF; int b = argb & 0xFF; + double minX = box.minX, minY = box.minY, minZ = box.minZ, maxX = box.maxX, maxY = box.maxY, maxZ = box.maxZ; + renderer.quad(minX, minY, minZ, maxX, minY, minZ, maxX, minY, maxZ, minX, minY, maxZ, r, g, b, a); + renderer.quad(minX, maxY, minZ, minX, maxY, maxZ, maxX, maxY, maxZ, maxX, maxY, minZ, r, g, b, a); + renderer.quad(minX, minY, maxZ, maxX, minY, maxZ, maxX, maxY, maxZ, minX, maxY, maxZ, r, g, b, a); + renderer.quad(minX, minY, minZ, minX, maxY, minZ, maxX, maxY, minZ, maxX, minY, minZ, r, g, b, a); + renderer.quad(maxX, minY, minZ, maxX, maxY, minZ, maxX, maxY, maxZ, maxX, minY, maxZ, r, g, b, a); + renderer.quad(minX, minY, minZ, minX, minY, maxZ, minX, maxY, maxZ, minX, maxY, minZ, r, g, b, a); + } + + private static void addOutlineBox(Renderer3D renderer, AABB box, int argb) { + int a = (argb >>> 24) & 0xFF; int r = (argb >>> 16) & 0xFF; int g = (argb >>> 8) & 0xFF; int b = argb & 0xFF; + double minX = box.minX, minY = box.minY, minZ = box.minZ, maxX = box.maxX, maxY = box.maxY, maxZ = box.maxZ; + renderer.line(minX, minY, minZ, maxX, minY, minZ, r, g, b, a); + renderer.line(maxX, minY, minZ, maxX, minY, maxZ, r, g, b, a); + renderer.line(maxX, minY, maxZ, minX, minY, maxZ, r, g, b, a); + renderer.line(minX, minY, maxZ, minX, minY, minZ, r, g, b, a); + renderer.line(minX, maxY, minZ, maxX, maxY, minZ, r, g, b, a); + renderer.line(maxX, maxY, minZ, maxX, maxY, maxZ, r, g, b, a); + renderer.line(maxX, maxY, maxZ, minX, maxY, maxZ, r, g, b, a); + renderer.line(minX, maxY, maxZ, minX, maxY, minZ, r, g, b, a); + renderer.line(minX, minY, minZ, minX, maxY, minZ, r, g, b, a); + renderer.line(maxX, minY, minZ, maxX, maxY, minZ, r, g, b, a); + renderer.line(maxX, minY, maxZ, maxX, maxY, maxZ, r, g, b, a); + renderer.line(minX, minY, maxZ, minX, maxY, maxZ, r, g, b, a); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsAim.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsAim.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsAim.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsAim.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsGeometry.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsGeometry.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsGeometry.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsGeometry.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsModel.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsModel.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsModel.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsModel.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsSimulation.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsSimulation.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsSimulation.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/predictions/PredictionsSimulation.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/PvpCooldowns.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/PvpCooldowns.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/PvpCooldowns.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/PvpCooldowns.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/RubberHand.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/RubberHand.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/RubberHand.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/RubberHand.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/SpearAssist.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/SpearAssist.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/SpearAssist.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/SpearAssist.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/TPSSync.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/TPSSync.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/TPSSync.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/pvp/TPSSync.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspCrystals.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspCrystals.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspCrystals.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspCrystals.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspEffects.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspEffects.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspEffects.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspEffects.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspModes.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspModes.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspModes.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspModes.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspRender.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspRender.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspRender.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspRender.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/Balancer.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/Balancer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/Balancer.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/Balancer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/ClickGui.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/ClickGui.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/ClickGui.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/ClickGui.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/DefineTarget.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/DefineTarget.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/DefineTarget.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/DefineTarget.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/FakePlayer.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/FakePlayer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/FakePlayer.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/FakePlayer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/esp/ShitEsp.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/esp/ShitEsp.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/esp/ShitEsp.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/esp/ShitEsp.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/items/ItemScroller.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/items/ItemScroller.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/items/ItemScroller.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/items/ItemScroller.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/items/ScrollMovePolicy.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/items/ScrollMovePolicy.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/items/ScrollMovePolicy.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/items/ScrollMovePolicy.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/protect/MessageFilter.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/protect/MessageFilter.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/protect/MessageFilter.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/protect/MessageFilter.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/protect/NameProtect.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/protect/NameProtect.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/protect/NameProtect.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/protect/NameProtect.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/protect/NoServerPack.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/protect/NoServerPack.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/protect/NoServerPack.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/protect/NoServerPack.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/protect/StreamerMode.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/protect/StreamerMode.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/protect/StreamerMode.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/protect/StreamerMode.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/sound/HitSounds.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/sound/HitSounds.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/sound/HitSounds.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/sound/HitSounds.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/sound/NoSound.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/sound/NoSound.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/sound/NoSound.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/sound/NoSound.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/vanilla/BetterMinecraft.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/vanilla/BetterMinecraft.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/vanilla/BetterMinecraft.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/misc/vanilla/BetterMinecraft.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/DeathCoords.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/DeathCoords.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/DeathCoords.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/player/DeathCoords.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/FakeLag.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/FakeLag.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/FakeLag.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/player/FakeLag.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/ThirdPerson.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/ThirdPerson.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/ThirdPerson.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/player/ThirdPerson.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/UseTracker.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/UseTracker.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/UseTracker.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/player/UseTracker.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/auto/AutoAccept.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/auto/AutoAccept.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/auto/AutoAccept.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/player/auto/AutoAccept.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/auto/AutoLeave.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/auto/AutoLeave.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/auto/AutoLeave.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/player/auto/AutoLeave.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/auto/AutoRespawn.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/auto/AutoRespawn.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/auto/AutoRespawn.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/player/auto/AutoRespawn.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/AutoTool.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/AutoTool.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/AutoTool.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/AutoTool.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/ClickPearl.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/ClickPearl.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/ClickPearl.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/ClickPearl.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/NoDelay.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/NoDelay.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/NoDelay.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/NoDelay.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/NoInteract.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/NoInteract.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/NoInteract.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/NoInteract.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/inventory/LockSlot.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/inventory/LockSlot.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/inventory/LockSlot.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/player/inventory/LockSlot.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/inventory/TapeMouse.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/player/inventory/TapeMouse.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/inventory/TapeMouse.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/player/inventory/TapeMouse.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/FovControl.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/FovControl.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/FovControl.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/FovControl.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/AspectRatio.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/AspectRatio.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/AspectRatio.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/AspectRatio.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/CameraClip.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/CameraClip.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/CameraClip.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/CameraClip.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/Freecam.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/Freecam.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/Freecam.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/Freecam.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/ViewModel.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/ViewModel.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/ViewModel.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/camera/ViewModel.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/Trails.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/Trails.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/Trails.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/Trails.java diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/WorldParticles.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/WorldParticles.java new file mode 100644 index 0000000..ce6382a --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/WorldParticles.java @@ -0,0 +1,260 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.visuals.effects.ambient; + +import java.util.ArrayList; +import java.util.List; +import net.minecraft.client.Minecraft; +import net.minecraft.util.Mth; +import net.minecraft.world.phys.Vec3; +import org.joml.Quaternionf; +import dev.loki.lovisual.config.common.CommonSettingSchemas; +import dev.loki.lovisual.config.values.color.RGBAColorValue; +import dev.loki.lovisual.config.values.color.RGBColorValue; +import dev.loki.lovisual.config.values.mode.EnumValue; +import dev.loki.lovisual.config.values.mode.ModeValue; +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import dev.loki.lovisual.features.theme.Theme; +import dev.loki.lovisual.features.module.core.Module; +import dev.loki.lovisual.features.module.core.ModuleCategory; +import dev.loki.lovisual.features.module.core.ModuleInfo; +import dev.loki.lovisual.features.module.phase.WorldPhase; +import dev.loki.lovisual.render.engine.RenderState; +import dev.loki.lovisual.render.engine.animation.AnimatedColorModeOptions; +import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors; +import dev.loki.lovisual.render.engine.color.ColorBlendMode; +import dev.loki.lovisual.render.engine.profiler.frame.RenderCostProfiler; +import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import dev.loki.lovisual.features.module.modules.visuals.effects.ambient.geometry.WorldParticlesGeometry; +import dev.loki.lovisual.features.module.modules.visuals.effects.ambient.geometry.WorldParticlesSphere; +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; + +//todo Description +@ModuleInfo( + id = "worldparticles", + displayName = "WorldParticles", + category = ModuleCategory.VISUALS +) +public class WorldParticles extends Module { + private static final float SPRITE_SIZE_MULTIPLIER = 4.0f; + private static final float SPRITE_ALPHA_MULTIPLIER = 0.40f; + private static final float DIAGONAL_ALPHA_MULTIPLIER = 0.40f; + private static final float OUTLINE_ALPHA_MULTIPLIER = 205.0f / 255.0f; + + private final EnumValue mode = + enumSetting("mode", "mode", Mode.QUADS); + private final BooleanValue depthTest = + boolCommon("depth_test", CommonSettingSchemas.RENDER_DEPTH_TEST, true); + private final RGBAColorValue color = + common(color("color", "#FF8ED4FF"), CommonSettingSchemas.RENDER_PRIMARY_COLOR.commonI18nKey()); + private final ModeValue colorMode = + modeSetting("worldParticlesColorMode", "color_mode", "Static", + AnimatedColorModeOptions.OPTIONS); + private final NumberValue colorSpeed = + num("worldParticlesColorSpeed", "color_speed", 18, 2, 54); + private final NumberValue colorPhase = + num("worldParticlesColorPhase", "color_phase", 0, 0, 360); + private final NumberValue colorSpread = + num("worldParticlesColorSpread", "color_spread", 90, -720, 720); + private final RGBAColorValue color2 = + visibleWhen(color("worldParticlesColor2", "color2", "#FF55FFFF"), this::usesSecondaryColor); + private final NumberValue maxParticles = + num("max_particles", 100, 1, 400); + private final NumberValue spawnPerTick = + num("spawn_per_tick", 1, 1, 20); + private final NumberValue radius = + num("radius", 20.0, 1.0, 64.0); + private final NumberValue height = + num("height", 5.0, 0.5, 16.0); + private final NumberValue lifeMinMs = + num("life_min_ms", 1500, 100, 10000); + private final NumberValue lifeMaxMs = + num("life_max_ms", 4500, 100, 15000); + private final NumberValue sizeMin = + num("size_min", 0.10, 0.02, 1.0); + private final NumberValue sizeMax = + num("size_max", 0.30, 0.02, 1.5); + private final NumberValue lineWidth = + visibleWhen(num("line_width", 1.0, 0.5, 4.0), () -> !isFunnelMode() && !isBubbleMode()); + private final EnumValue funnelTintMode = + visibleWhen(enumSetting("funnel_tint_mode", "funnel_tint_mode", ColorBlendMode.ORIGINAL), this::isFunnelMode); + private final BooleanValue funnelTintSyncTheme = + visibleWhen(bool("funnel_tint_sync_theme", "funnel_tint_sync_theme", true), this::isFunnelTintControlsVisible); + private final RGBColorValue funnelTintColor = + visibleWhen(colorNoAlpha("funnel_tint_color", "funnel_tint_color", "#78D7FF"), this::isManualFunnelTintVisible); + private final NumberValue funnelTintStrength = + visibleWhen(num("funnel_tint_strength", "funnel_tint_strength", 0.45f, 0.0f, 1.0f), this::isFunnelTintControlsVisible); + private final List particles = new ArrayList<>(); + + @Override + public WorldPhase getWorldPhase() { + return WorldPhase.END_MAIN; + } + + @Override + public void onDisable() { + particles.clear(); + } + + @Override + public void onTick() { + Minecraft mc = Minecraft.getInstance(); + if (!isEnabled() || mc.player == null || mc.level == null) { + return; + } + + particles.removeIf(WorldParticlesParticle::isExpired); + for (WorldParticlesParticle particle : particles) { + particle.tick(); + } + + int targetCount = Math.max(1, maxParticles.get()); + int available = targetCount - particles.size(); + if (available <= 0) { + return; + } + + int spawnCount = Mth.clamp(spawnPerTick.get(), 0, available); + for (int i = 0; i < spawnCount; i++) { + particles.add(WorldParticlesParticle.spawn(radius.get(), height.get(), + lifeMinMs.get(), lifeMaxMs.get(), sizeMin.get(), sizeMax.get(), isBubbleMode())); + } + } + + @Override + public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); + if (!isEnabled() || particles.isEmpty() || mc.player == null || mc.level == null) { + return; + } + + try (RenderCostProfiler.Scope ignored = RenderCostProfiler.worldEffect("world_particles:" + mode.get())) { + boolean useDepth = depthTest.get(); + Renderer3D.DepthMode depthMode = useDepth ? Renderer3D.DepthMode.PRE_DEPTH : Renderer3D.DepthMode.MAIN; + if (isFunnelMode()) { + WorldParticlesFunnel.render(renderer, useDepth, depthMode, tickDelta, particles, + new WorldParticlesFunnel.Tint(funnelTintRgb(), funnelTintMode.get(), funnelTintStrength.get()), + this::colorForParticle); + return; + } + + boolean bubbleMode = isBubbleMode(); + MeshBuilder spriteMesh = WorldParticlesBatches.sprite(renderer, useDepth, depthMode); + MeshBuilder lineMesh = bubbleMode + ? null + : WorldParticlesBatches.lines(renderer, useDepth, depthMode, lineWidth.get().floatValue()); + MeshBuilder bubbleShellMesh = bubbleMode + ? WorldParticlesBatches.bubbleShell(renderer, useDepth, depthMode) + : null; + + int particleCount = particles.size(); + if (bubbleShellMesh != null) { + bubbleShellMesh.ensureCapacity( + particleCount * (WorldParticlesSphere.STACKS + 1) * (WorldParticlesSphere.SLICES + 1), + particleCount * WorldParticlesSphere.STACKS * WorldParticlesSphere.SLICES * 6 + ); + } + if (spriteMesh != null) { + spriteMesh.ensureCapacity( + bubbleMode ? particleCount * 20 : particleCount * 4, + bubbleMode ? particleCount * 30 : particleCount * 6 + ); + } + if (lineMesh != null) { + lineMesh.ensureCapacity(particleCount * 32, particleCount * 32); + } + + Quaternionf camRot = RenderState.cameraRotation; + for (int i = 0; i < particles.size(); i++) { + WorldParticlesParticle particle = particles.get(i); + float alpha = particle.alpha(); + if (alpha <= 0.003f) { + continue; + } + + Vec3 pos = particle.interpolatePos(tickDelta); + int baseColor = colorForParticle(particle, i); + + if (bubbleMode) { + WorldParticlesBubble.renderParticle(bubbleShellMesh, spriteMesh, particle, pos, camRot, alpha, baseColor); + continue; + } + + if (spriteMesh != null) { + int spriteArgb = WorldParticlesGeometry.multiplyAlpha(baseColor, alpha * SPRITE_ALPHA_MULTIPLIER); + WorldParticlesGeometry.addBillboardQuad(spriteMesh, pos.x, pos.y, pos.z, + particle.size() * SPRITE_SIZE_MULTIPLIER, camRot, spriteArgb); + } + + if (lineMesh != null) { + Vec3 rotation = particle.interpolateRotation(tickDelta); + int diagonalArgb = WorldParticlesGeometry.multiplyAlpha(baseColor, alpha * DIAGONAL_ALPHA_MULTIPLIER); + int outlineArgb = WorldParticlesGeometry.multiplyAlpha(baseColor, alpha * OUTLINE_ALPHA_MULTIPLIER); + WorldParticlesGeometry.addCube(lineMesh, pos, rotation, particle.size(), diagonalArgb, outlineArgb); + } + } + } + } + + private int colorForParticle(WorldParticlesParticle particle, int index) { + int phase = colorPhase.get() + + Math.round(index * colorSpread.get() / 90.0f) + + Math.round(particle.visualPhase() * 57.29578f); + return AnimatedRenderColors.resolve( + AnimatedColorModeOptions.parse(colorMode.get()), + colorSpeed.get(), + phase, + color.getArgb(), + color2.getArgb(), + true + ); + } + + private boolean usesSecondaryColor() { + return AnimatedRenderColors.usesSecondary(AnimatedColorModeOptions.parse(colorMode.get())); + } + + private int funnelTintRgb() { + if (funnelTintSyncTheme.get()) { + return Theme.theme().accent() & 0x00FFFFFF; + } + return funnelTintColor.getArgb() & 0x00FFFFFF; + } + + private boolean isFunnelTintControlsVisible() { + return isFunnelMode() && funnelTintMode.get() != ColorBlendMode.ORIGINAL; + } + + private boolean isManualFunnelTintVisible() { + return isFunnelTintControlsVisible() && !funnelTintSyncTheme.get(); + } + + private boolean isFunnelMode() { + return mode.get() == Mode.FUNNEL; + } + + private boolean isBubbleMode() { + return mode.get() == Mode.AIR_BUBBLES; + } + + private enum Mode implements EnumValue.AliasProvider { + QUADS, + AIR_BUBBLES, + FUNNEL; + + @Override + public List aliases() { + return switch (this) { + case AIR_BUBBLES -> List.of("AirBubbles"); + case QUADS -> List.of("Quads"); + case FUNNEL -> List.of("Funnel"); + }; + } + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/geometry/WorldParticlesGeometry.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/geometry/WorldParticlesGeometry.java similarity index 88% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/geometry/WorldParticlesGeometry.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/geometry/WorldParticlesGeometry.java index c929c58..57ab23a 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/geometry/WorldParticlesGeometry.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/geometry/WorldParticlesGeometry.java @@ -1,5 +1,6 @@ package dev.loki.lovisual.features.module.modules.visuals.effects.ambient.geometry; +import net.minecraft.util.Mth; import net.minecraft.world.phys.Vec3; import org.joml.Quaternionf; import org.joml.Vector3f; @@ -12,6 +13,19 @@ import dev.loki.lovisual.render.engine.uniform.MeshBuilder; public final class WorldParticlesGeometry { private WorldParticlesGeometry() {} + /** Смещение в плоскости билборда (право/верх камеры). */ + public static Vec3 offsetInBillboardPlane(Vec3 pos, Quaternionf camRot, float angle, float distance) { + Vector3f right = new Vector3f(1.0f, 0.0f, 0.0f).rotate(camRot).mul((float) Math.cos(angle) * distance); + Vector3f up = new Vector3f(0.0f, 1.0f, 0.0f).rotate(camRot).mul((float) Math.sin(angle) * distance); + return pos.add(right.x() + up.x(), right.y() + up.y(), right.z() + up.z()); + } + + public static int multiplyAlpha(int argb, float alphaMultiplier) { + int alpha = (argb >>> 24) & 0xFF; + int outAlpha = Mth.clamp(Math.round(alpha * alphaMultiplier), 0, 255); + return (outAlpha << 24) | (argb & 0x00FFFFFF); + } + public static void addBillboardQuad(MeshBuilder mesh, double cx, double cy, double cz, float size, Quaternionf camRot, int argb) { addBillboardQuad(mesh, cx, cy, cz, size, camRot, 0.0f, argb); } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/geometry/WorldParticlesSphere.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/geometry/WorldParticlesSphere.java similarity index 89% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/geometry/WorldParticlesSphere.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/geometry/WorldParticlesSphere.java index 17c8cfb..5faefd6 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/geometry/WorldParticlesSphere.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/geometry/WorldParticlesSphere.java @@ -14,6 +14,9 @@ import dev.loki.lovisual.render.engine.uniform.MeshBuilder; public final class WorldParticlesSphere { private WorldParticlesSphere() {} + public static final int STACKS = 5; + public static final int SLICES = 12; + public static Vector3f spherePoint(Vec3 center, Quaternionf rotation, float radius, float theta, float phi) { float sinTheta = (float) Math.sin(theta); return new Vector3f((float) Math.cos(phi) * sinTheta, (float) Math.cos(theta), (float) Math.sin(phi) * sinTheta).rotate(rotation).mul(radius).add((float) center.x, (float) center.y, (float) center.z); @@ -26,7 +29,7 @@ public final class WorldParticlesSphere { public static void addSphereShell(MeshBuilder mesh, Vec3 center, Quaternionf rotation, float radius, int baseColor, int brightColor, float alpha) { Vector3f light = new Vector3f(-0.36f, 0.84f, -0.40f).normalize(); - int stacks = 5, slices = 12; + int stacks = STACKS, slices = SLICES; int rowSize = slices + 1; int baseVertex = mesh.getVertexCount(); mesh.ensureCapacity((stacks + 1) * rowSize, stacks * slices * 6); @@ -56,12 +59,6 @@ public final class WorldParticlesSphere { int shadowColor = AnimatedRenderColors.mixArgb(baseColor, 0xFF000000, 0.38f); int midColor = AnimatedRenderColors.mixArgb(shadowColor, baseColor, 0.36f + vertical * 0.24f); int mixed = AnimatedRenderColors.mixArgb(midColor, brightColor, Mth.clamp(lit * 0.74f + rim * 0.24f, 0.0f, 1.0f)); - return multiplyAlpha(mixed, alpha * (0.22f + lit * 0.22f + rim * 0.16f)); - } - - private static int multiplyAlpha(int argb, float alphaMultiplier) { - int alpha = (argb >>> 24) & 0xFF; - int outAlpha = Mth.clamp(Math.round(alpha * alphaMultiplier), 0, 255); - return (outAlpha << 24) | (argb & 0x00FFFFFF); + return WorldParticlesGeometry.multiplyAlpha(mixed, alpha * (0.22f + lit * 0.22f + rim * 0.16f)); } } diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/particle/WorldParticlesParticle.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/particle/WorldParticlesParticle.java new file mode 100644 index 0000000..989e256 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/particle/WorldParticlesParticle.java @@ -0,0 +1,197 @@ +package dev.loki.lovisual.features.module.modules.visuals.effects.ambient; + +import net.minecraft.client.Minecraft; +import net.minecraft.util.Mth; +import net.minecraft.world.phys.Vec3; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.util.time.GameClock; + +import java.util.concurrent.ThreadLocalRandom; + +/** + * Состояние одной частицы WorldParticles: позиция, вращение, дрейф и + * анимации прозрачности. Спавн — {@link #spawn}, физика — {@link #tick()}. + */ +final class WorldParticlesParticle { + private static final float MOTION_SCALE = 0.04f; + private static final float DRAG = 0.98f; + private static final long ALPHA_ANIM_MS = 1000L; + + private final long spawnMs; + private final long lifeMs; + private final float size; + private final float visualPhase; + private final float eyeSpin; + private final float distortionSpin; + private Vec3 prevPos; + private Vec3 pos; + private Vec3 prevRotation; + private Vec3 rotation; + private Vec3 motion; + private Vec3 rotationMotion; + + private WorldParticlesParticle(Vec3 pos, Vec3 motion, Vec3 rotationMotion, long lifeMs, float size, + float visualPhase, float eyeSpin, float distortionSpin) { + this.prevPos = pos; + this.pos = pos; + this.prevRotation = Vec3.ZERO; + this.rotation = Vec3.ZERO; + this.motion = motion; + this.rotationMotion = rotationMotion; + this.spawnMs = GameClock.millis(); + this.lifeMs = Math.max(1L, lifeMs); + this.size = size; + this.visualPhase = visualPhase; + this.eyeSpin = eyeSpin; + this.distortionSpin = distortionSpin; + } + + static WorldParticlesParticle spawn(double radius, double height, int lifeMinMs, int lifeMaxMs, + double sizeMin, double sizeMax, boolean bubbleMode) { + Minecraft mc = Minecraft.getInstance(); + ThreadLocalRandom random = ThreadLocalRandom.current(); + Vec3 playerPos = mc.player.position(); + + double range = Math.max(0.1, radius); + double heightRange = Math.max(0.1, height); + double px = playerPos.x + random.nextDouble(-range, range); + double py = playerPos.y + random.nextDouble(0.0, heightRange); + double pz = playerPos.z + random.nextDouble(-range, range); + + Vec3 pos = new Vec3(px, py, pz); + Vec3 motion = bubbleMode + ? new Vec3( + random.nextDouble(-0.42, 0.42) * MOTION_SCALE, + random.nextDouble(1.10, 2.55) * MOTION_SCALE, + random.nextDouble(-0.42, 0.42) * MOTION_SCALE + ) + : new Vec3( + random.nextDouble(-1.0, 1.0) * MOTION_SCALE, + random.nextDouble(0.0, 2.0) * MOTION_SCALE, + random.nextDouble(-1.0, 1.0) * MOTION_SCALE + ); + Vec3 rotationMotion = new Vec3( + random.nextDouble(-1.0, 1.0) * MOTION_SCALE, + random.nextDouble(-1.0, 1.0) * MOTION_SCALE, + random.nextDouble(-1.0, 1.0) * MOTION_SCALE + ); + + int minLife = Math.max(1, Math.min(lifeMinMs, lifeMaxMs)); + int maxLife = Math.max(minLife, Math.max(lifeMinMs, lifeMaxMs)); + long life = randomLongClosed(random, minLife, maxLife); + + double minSize = Math.max(0.02, Math.min(sizeMin, sizeMax)); + double maxSize = Math.max(minSize, Math.max(sizeMin, sizeMax)); + float size = (float) randomDoubleClosed(random, minSize, maxSize); + + float visualPhase = (float) random.nextDouble(0.0, Math.PI * 2.0); + float eyeSpin = (float) random.nextDouble(0.38, 0.72); + float distortionSpin = (float) random.nextDouble(-0.28, -0.12); + + return new WorldParticlesParticle(pos, motion, rotationMotion, life, size, visualPhase, eyeSpin, distortionSpin); + } + + void tick() { + prevPos = pos; + prevRotation = rotation; + pos = pos.add(motion); + rotation = rotation.add(rotationMotion); + motion = motion.scale(DRAG); + rotationMotion = rotationMotion.scale(DRAG); + } + + float alpha() { + long age = GameClock.millis() - spawnMs; + if (age <= 0L) { + return 0.0f; + } + if (age < ALPHA_ANIM_MS) { + return AnimationUtility.smoothstep(age / (float) ALPHA_ANIM_MS); + } + if (age < lifeMs) { + return 1.0f; + } + float fade = 1.0f - (age - lifeMs) / (float) ALPHA_ANIM_MS; + return AnimationUtility.smoothstep(Mth.clamp(fade, 0.0f, 1.0f)); + } + + float funnelAlpha() { + long age = ageMs(); + if (age <= 0L) { + return 0.0f; + } + + float appear = AnimationUtility.easeOutCubic(age / 720.0f); + float disappear = 1.0f; + if (age > lifeMs) { + disappear = 1.0f - AnimationUtility.easeInCubic((age - lifeMs) / (float) ALPHA_ANIM_MS); + } + + float breath = 0.88f + 0.12f * (float) Math.sin(age * 0.0032f + visualPhase); + return AnimationUtility.clamp01(appear * disappear * breath); + } + + float funnelScale() { + long age = ageMs(); + float appear = AnimationUtility.easeOutCubic(age / 900.0f); + float disappear = 1.0f; + if (age > lifeMs) { + disappear = 1.0f - AnimationUtility.smoothstep((age - lifeMs) / (float) ALPHA_ANIM_MS); + } + + float open = AnimationUtility.lerp(0.68f, 1.04f, appear); + float close = AnimationUtility.lerp(0.82f, 1.0f, disappear); + return open * close; + } + + long ageMs() { + return GameClock.millis() - spawnMs; + } + + float lifeProgress() { + return AnimationUtility.clamp01(ageMs() / (float) Math.max(1L, lifeMs)); + } + + boolean isExpired() { + return GameClock.millis() - spawnMs >= lifeMs + ALPHA_ANIM_MS; + } + + Vec3 interpolatePos(float tickDelta) { + return prevPos.lerp(pos, tickDelta); + } + + Vec3 interpolateRotation(float tickDelta) { + return prevRotation.lerp(rotation, tickDelta); + } + + float size() { + return size; + } + + float visualPhase() { + return visualPhase; + } + + float eyeSpin() { + return eyeSpin; + } + + float distortionSpin() { + return distortionSpin; + } + + private static double randomDoubleClosed(ThreadLocalRandom random, double min, double max) { + if (!Double.isFinite(min) || !Double.isFinite(max)) return 0.02; + double low = Math.min(min, max); + double high = Math.max(min, max); + return high <= low ? low : random.nextDouble(low, high); + } + + private static long randomLongClosed(ThreadLocalRandom random, long min, long max) { + long low = Math.min(min, max); + long high = Math.max(min, max); + if (high <= low) return low; + if (high == Long.MAX_VALUE) return random.nextLong(low, Long.MAX_VALUE) + 1L; + return random.nextLong(low, high + 1L); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/render/WorldParticlesBatches.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/render/WorldParticlesBatches.java new file mode 100644 index 0000000..ab4ab2b --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/render/WorldParticlesBatches.java @@ -0,0 +1,61 @@ +package dev.loki.lovisual.features.module.modules.visuals.effects.ambient; + +import net.minecraft.resources.Identifier; +import dev.loki.lovisual.render.engine.RenderState; +import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; +import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import dev.loki.lovisual.render.engine.texture.TextureStorage; +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; +import dev.loki.lovisual.render.engine.uniform.impl.hand.effects.TextureTintUniforms; + +/** + * Фабрики батчей WorldParticles: спрайты, линии, оболочка пузыря, + * тонированная текстурная пачка (воронка). + */ +final class WorldParticlesBatches { + private WorldParticlesBatches() {} + + static MeshBuilder sprite(Renderer3D renderer, boolean useDepth, Renderer3D.DepthMode depthMode) { + return renderer.batchTextured( + useDepth ? LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE_LIQUID_IGNORE + : LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE, + TextureStorage.BLOOM, + depthMode + ); + } + + static MeshBuilder lines(Renderer3D renderer, boolean useDepth, Renderer3D.DepthMode depthMode, float width) { + float prevLineWidth = RenderState.lineWidth; + try { + RenderState.lineWidth = Math.max(0.5f, width); + return renderer.batch( + useDepth ? LoVisualRenderPipelines.WORLD_COLORED_LINES_LIQUID_IGNORE + : LoVisualRenderPipelines.WORLD_COLORED_LINES, + depthMode + ); + } finally { + RenderState.lineWidth = prevLineWidth; + } + } + + static MeshBuilder bubbleShell(Renderer3D renderer, boolean useDepth, Renderer3D.DepthMode depthMode) { + return renderer.batch( + useDepth ? LoVisualRenderPipelines.WORLD_COLORED_LIQUID_IGNORE + : LoVisualRenderPipelines.WORLD_COLORED, + depthMode + ); + } + + static MeshBuilder tintedTexture(Renderer3D renderer, boolean useDepth, Renderer3D.DepthMode depthMode, + Identifier texture) { + return renderer.batch( + useDepth + ? LoVisualRenderPipelines.WORLD_TEXTURED_TINT_ADDITIVE_LIQUID_IGNORE + : LoVisualRenderPipelines.WORLD_TEXTURED_TINT_ADDITIVE, + depthMode, + Renderer3D.BatchBindings.none() + .withSampler("u_Texture", texture) + .withUniform("TextureTint", TextureTintUniforms.get()) + ); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/render/WorldParticlesBubble.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/render/WorldParticlesBubble.java new file mode 100644 index 0000000..d3e50a9 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/render/WorldParticlesBubble.java @@ -0,0 +1,137 @@ +package dev.loki.lovisual.features.module.modules.visuals.effects.ambient; + +import net.minecraft.world.phys.Vec3; +import dev.loki.lovisual.features.module.modules.visuals.effects.ambient.geometry.WorldParticlesGeometry; +import dev.loki.lovisual.features.module.modules.visuals.effects.ambient.geometry.WorldParticlesSphere; +import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; +import org.joml.Quaternionf; + +/** + * Режим AIR_BUBBLES: воздушный пузырь — оболочка-сфера + глоу, заливка, + * блик и вторичный блик в плоскости билборда. + */ +final class WorldParticlesBubble { + private static final float FILL_SIZE_MULTIPLIER = 3.15f; + private static final float GLOW_SIZE_MULTIPLIER = 5.10f; + private static final float HIGHLIGHT_SIZE_MULTIPLIER = 0.34f; + private static final float SECONDARY_HIGHLIGHT_SIZE_MULTIPLIER = 0.20f; + private static final float FILL_ALPHA_MULTIPLIER = 0.34f; + private static final float GLOW_ALPHA_MULTIPLIER = 0.13f; + private static final float DEPTH_ALPHA_MULTIPLIER = 0.22f; + private static final float HIGHLIGHT_ALPHA_MULTIPLIER = 0.74f; + + private WorldParticlesBubble() {} + + static void renderParticle(MeshBuilder shellMesh, + MeshBuilder spriteMesh, + WorldParticlesParticle particle, + Vec3 pos, + Quaternionf camRot, + float alpha, + int baseColor) { + float ageSeconds = particle.ageMs() / 1000.0f; + float lifeT = particle.lifeProgress(); + float pulse = 0.5f + 0.5f * (float) Math.sin(ageSeconds * 3.1f + particle.visualPhase()); + float pop = 1.0f + AnimationUtility.smoothstep(pulse) * 0.09f; + float radius = particle.size() * FILL_SIZE_MULTIPLIER * pop; + float lightAngle = particle.visualPhase() + 2.35f + ageSeconds * 0.16f; + float depthAngle = lightAngle + (float) Math.PI; + int brightColor = AnimatedRenderColors.mixArgb(baseColor, 0xFFFFFFFF, 0.72f); + int softColor = AnimatedRenderColors.mixArgb(baseColor, 0xFFFFFFFF, 0.32f); + int shadowColor = AnimatedRenderColors.mixArgb(baseColor, 0xFF000000, 0.42f); + Quaternionf sphereRotation = new Quaternionf() + .rotateY(particle.visualPhase() + ageSeconds * 0.34f) + .rotateX(particle.visualPhase() * 0.47f + ageSeconds * 0.22f) + .rotateZ(particle.visualPhase() * -0.31f + ageSeconds * 0.16f); + float sphereRadius = radius * 0.82f; + + if (shellMesh != null) { + WorldParticlesSphere.addSphereShell(shellMesh, pos, sphereRotation, sphereRadius, baseColor, brightColor, alpha); + } + + if (spriteMesh == null) { + return; + } + + WorldParticlesGeometry.addBillboardQuadGradient( + spriteMesh, + pos.x, pos.y, pos.z, + particle.size() * GLOW_SIZE_MULTIPLIER * pop, + camRot, + particle.visualPhase() * 0.22f + ageSeconds * 0.12f, + WorldParticlesGeometry.multiplyAlpha(shadowColor, alpha * GLOW_ALPHA_MULTIPLIER * 0.58f), + WorldParticlesGeometry.multiplyAlpha(baseColor, alpha * GLOW_ALPHA_MULTIPLIER * 0.88f), + WorldParticlesGeometry.multiplyAlpha(brightColor, alpha * GLOW_ALPHA_MULTIPLIER * 1.32f), + WorldParticlesGeometry.multiplyAlpha(softColor, alpha * GLOW_ALPHA_MULTIPLIER) + ); + + renderDepthShade(spriteMesh, pos, camRot, radius, depthAngle, particle.visualPhase(), pulse, alpha, baseColor, brightColor, softColor, shadowColor); + + float fillAlpha = alpha * FILL_ALPHA_MULTIPLIER; + WorldParticlesGeometry.addBillboardQuadGradient( + spriteMesh, + pos.x, pos.y, pos.z, + radius, + camRot, + particle.visualPhase() * -0.16f, + WorldParticlesGeometry.multiplyAlpha(shadowColor, fillAlpha * 0.92f), + WorldParticlesGeometry.multiplyAlpha(baseColor, fillAlpha * 1.06f), + WorldParticlesGeometry.multiplyAlpha(brightColor, fillAlpha * 0.98f), + WorldParticlesGeometry.multiplyAlpha(softColor, fillAlpha * 1.18f) + ); + + Vec3 highlight = WorldParticlesGeometry.offsetInBillboardPlane(pos, camRot, lightAngle, radius * 0.42f); + float highlightAlpha = alpha * HIGHLIGHT_ALPHA_MULTIPLIER * (1.0f - lifeT * 0.22f); + WorldParticlesGeometry.addBillboardQuadGradient( + spriteMesh, + highlight.x, highlight.y, highlight.z, + particle.size() * HIGHLIGHT_SIZE_MULTIPLIER * (0.74f + pulse * 0.22f), + camRot, + particle.visualPhase(), + WorldParticlesGeometry.multiplyAlpha(softColor, highlightAlpha * 0.46f), + WorldParticlesGeometry.multiplyAlpha(brightColor, highlightAlpha * 0.72f), + WorldParticlesGeometry.multiplyAlpha(0xFFFFFFFF, highlightAlpha), + WorldParticlesGeometry.multiplyAlpha(brightColor, highlightAlpha * 0.86f) + ); + + renderSecondaryHighlight(spriteMesh, pos, camRot, radius, particle.size(), lightAngle, particle.visualPhase(), pulse, alpha, baseColor, brightColor, softColor); + } + + private static void renderDepthShade(MeshBuilder spriteMesh, Vec3 pos, Quaternionf camRot, float radius, + float depthAngle, float visualPhase, float pulse, float alpha, + int baseColor, int brightColor, int softColor, int shadowColor) { + Vec3 depth = WorldParticlesGeometry.offsetInBillboardPlane(pos, camRot, depthAngle, radius * 0.22f); + float depthAlpha = alpha * DEPTH_ALPHA_MULTIPLIER * (0.78f + pulse * 0.18f); + WorldParticlesGeometry.addBillboardQuadGradient( + spriteMesh, + depth.x, depth.y, depth.z, + radius * 0.58f, + camRot, + visualPhase * 0.31f, + WorldParticlesGeometry.multiplyAlpha(shadowColor, depthAlpha * 0.92f), + WorldParticlesGeometry.multiplyAlpha(baseColor, depthAlpha * 0.76f), + WorldParticlesGeometry.multiplyAlpha(softColor, depthAlpha * 0.42f), + WorldParticlesGeometry.multiplyAlpha(shadowColor, depthAlpha * 0.58f) + ); + } + + private static void renderSecondaryHighlight(MeshBuilder spriteMesh, Vec3 pos, Quaternionf camRot, float radius, + float size, float lightAngle, float visualPhase, float pulse, float alpha, + int baseColor, int brightColor, int softColor) { + Vec3 secondaryHighlight = WorldParticlesGeometry.offsetInBillboardPlane(pos, camRot, lightAngle - 0.92f, radius * 0.18f); + float secondaryHighlightAlpha = alpha * HIGHLIGHT_ALPHA_MULTIPLIER * 0.42f * (0.84f + pulse * 0.16f); + WorldParticlesGeometry.addBillboardQuadGradient( + spriteMesh, + secondaryHighlight.x, secondaryHighlight.y, secondaryHighlight.z, + size * SECONDARY_HIGHLIGHT_SIZE_MULTIPLIER, + camRot, + visualPhase * -0.38f, + WorldParticlesGeometry.multiplyAlpha(baseColor, secondaryHighlightAlpha * 0.44f), + WorldParticlesGeometry.multiplyAlpha(brightColor, secondaryHighlightAlpha * 0.68f), + WorldParticlesGeometry.multiplyAlpha(0xFFFFFFFF, secondaryHighlightAlpha * 0.82f), + WorldParticlesGeometry.multiplyAlpha(softColor, secondaryHighlightAlpha * 0.58f) + ); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/render/WorldParticlesFunnel.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/render/WorldParticlesFunnel.java new file mode 100644 index 0000000..6c2bf52 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/render/WorldParticlesFunnel.java @@ -0,0 +1,182 @@ +package dev.loki.lovisual.features.module.modules.visuals.effects.ambient; + +import net.minecraft.util.Mth; +import net.minecraft.world.phys.Vec3; +import dev.loki.lovisual.features.module.modules.visuals.effects.ambient.geometry.WorldParticlesGeometry; +import dev.loki.lovisual.features.module.modules.visuals.optimization.FpsOptimizer; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.color.ColorBlendMode; +import dev.loki.lovisual.render.engine.RenderState; +import dev.loki.lovisual.render.engine.color.ColorEffects; +import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import dev.loki.lovisual.render.engine.texture.TextureStorage; +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; +import dev.loki.lovisual.render.engine.uniform.impl.hand.effects.TextureTintUniforms; +import org.joml.Quaternionf; + +import java.util.List; + +/** + * Режим FUNNEL: воронка — дисторшн-кольца, «глаз», bloom-лики и вспышка. + */ +final class WorldParticlesFunnel { + private static final float EYE_SIZE_MULTIPLIER = 4.35f; + private static final float DISTORTION_SIZE_MULTIPLIER = 5.25f; + private static final float DISTORTION_OUTER_SIZE_MULTIPLIER = 6.85f; + private static final float DISTORTION_INNER_SIZE_MULTIPLIER = 3.95f; + private static final float EYE_ALPHA_MULTIPLIER = 0.72f; + private static final float DISTORTION_ALPHA_MULTIPLIER = 0.16f; + private static final float DISTORTION_OUTER_ALPHA_MULTIPLIER = 0.085f; + private static final float DISTORTION_INNER_ALPHA_MULTIPLIER = 0.115f; + private static final float RIM_ALPHA_MULTIPLIER = 0.22f; + private static final float GHOST_ALPHA_MULTIPLIER = 0.18f; + private static final float FLASH_ALPHA_MULTIPLIER = 0.34f; + private static final float LEAK_ALPHA_MULTIPLIER = 0.32f; + + interface ColorProvider { + int colorFor(WorldParticlesParticle particle, int index); + } + + record Tint(int rgb, ColorBlendMode mode, float strength) {} + + private WorldParticlesFunnel() {} + + static void render(Renderer3D renderer, boolean useDepth, Renderer3D.DepthMode depthMode, float tickDelta, + List particles, Tint tint, ColorProvider colors) { + TextureTintUniforms.update( + tint.rgb(), + tint.mode() == ColorBlendMode.ORIGINAL ? 0.0f : Mth.clamp(tint.strength(), 0.0f, 1.0f), + tint.mode().ordinal() + ); + + MeshBuilder distortionMesh = WorldParticlesBatches.tintedTexture(renderer, useDepth, depthMode, TextureStorage.FUNNEL_DISTORTION); + MeshBuilder eyeMesh = WorldParticlesBatches.tintedTexture(renderer, useDepth, depthMode, TextureStorage.FUNNEL_EYE); + MeshBuilder bloomMesh = FpsOptimizer.bloomDisabled() + ? null + : WorldParticlesBatches.tintedTexture(renderer, useDepth, depthMode, TextureStorage.BLOOM); + if (distortionMesh == null && eyeMesh == null && bloomMesh == null) { + return; + } + + int particleCount = particles.size(); + if (distortionMesh != null) { + distortionMesh.ensureCapacity(particleCount * 12, particleCount * 18); + } + if (eyeMesh != null) { + eyeMesh.ensureCapacity(particleCount * 16, particleCount * 24); + } + if (bloomMesh != null) { + bloomMesh.ensureCapacity(particleCount * 20, particleCount * 30); + } + + Quaternionf camRot = RenderState.cameraRotation; + for (int i = 0; i < particles.size(); i++) { + WorldParticlesParticle particle = particles.get(i); + float alpha = particle.funnelAlpha(); + if (alpha <= 0.003f) { + continue; + } + + Vec3 pos = particle.interpolatePos(tickDelta); + int baseColor = colors.colorFor(particle, i); + float ageSeconds = particle.ageMs() / 1000.0f; + float scale = particle.funnelScale(); + float pulse = 0.5f + 0.5f * (float) Math.sin(particle.ageMs() * 0.0048f + particle.visualPhase()); + float rimPulse = AnimationUtility.smoothstep(pulse); + + renderDistortion(distortionMesh, particle, pos, camRot, alpha, scale, baseColor, ageSeconds, rimPulse); + renderEye(eyeMesh, particle, pos, camRot, alpha, scale, baseColor, ageSeconds, rimPulse); + + if (bloomMesh != null) { + renderLeaks(bloomMesh, particle, pos, camRot, alpha, scale, baseColor, ageSeconds); + renderFlash(bloomMesh, particle, pos, camRot, alpha, scale, baseColor); + } + } + } + + private static void renderDistortion(MeshBuilder mesh, WorldParticlesParticle particle, Vec3 pos, + Quaternionf camRot, float alpha, float scale, int baseColor, + float ageSeconds, float rimPulse) { + if (mesh == null) return; + float outerRoll = particle.visualPhase() * 0.63f - ageSeconds * particle.distortionSpin() * 0.54f; + int outerArgb = funnelColor(baseColor, 0xFFFFFF, alpha * DISTORTION_OUTER_ALPHA_MULTIPLIER * (0.82f + rimPulse * 0.18f)); + addQuad(mesh, pos, camRot, + particle.size() * DISTORTION_OUTER_SIZE_MULTIPLIER * scale * (1.0f + rimPulse * 0.055f), + outerRoll, outerArgb); + + float distortionRoll = particle.visualPhase() + ageSeconds * particle.distortionSpin(); + int distortionArgb = funnelColor(baseColor, 0xFFFFFF, alpha * DISTORTION_ALPHA_MULTIPLIER); + addQuad(mesh, pos, camRot, particle.size() * DISTORTION_SIZE_MULTIPLIER * scale, distortionRoll, distortionArgb); + + float innerRoll = -particle.visualPhase() * 1.18f + ageSeconds * (0.18f - particle.distortionSpin() * 0.35f); + int innerArgb = funnelColor(baseColor, 0xFFFFFF, alpha * DISTORTION_INNER_ALPHA_MULTIPLIER * (1.0f - rimPulse * 0.25f)); + addQuad(mesh, pos, camRot, particle.size() * DISTORTION_INNER_SIZE_MULTIPLIER * scale, innerRoll, innerArgb); + } + + private static void renderEye(MeshBuilder mesh, WorldParticlesParticle particle, Vec3 pos, + Quaternionf camRot, float alpha, float scale, int baseColor, + float ageSeconds, float rimPulse) { + if (mesh == null) return; + float eyeRoll = -particle.visualPhase() * 0.42f + ageSeconds * particle.eyeSpin(); + int rimArgb = funnelColor(baseColor, 0xFFC76A, alpha * RIM_ALPHA_MULTIPLIER * (0.62f + rimPulse * 0.38f)); + addQuad(mesh, pos, camRot, + particle.size() * (EYE_SIZE_MULTIPLIER + 0.48f) * scale * (1.0f + rimPulse * 0.045f), + eyeRoll - 0.18f, rimArgb); + + int ghostA = funnelColor(baseColor, 0x92E8FF, alpha * GHOST_ALPHA_MULTIPLIER * (0.70f + rimPulse * 0.16f)); + addQuad(mesh, pos, camRot, particle.size() * 3.74f * scale, eyeRoll + 0.74f, ghostA); + + int ghostB = funnelColor(baseColor, 0xFF9D47, alpha * GHOST_ALPHA_MULTIPLIER * 0.68f * (1.0f - rimPulse * 0.18f)); + addQuad(mesh, pos, camRot, particle.size() * 4.88f * scale, eyeRoll - 1.06f, ghostB); + + int eyeArgb = funnelColor(baseColor, 0xFFFFFF, alpha * EYE_ALPHA_MULTIPLIER); + addQuad(mesh, pos, camRot, particle.size() * EYE_SIZE_MULTIPLIER * scale, eyeRoll, eyeArgb); + } + + private static void renderFlash(MeshBuilder mesh, WorldParticlesParticle particle, Vec3 pos, + Quaternionf camRot, float alpha, float scale, int baseColor) { + float lifeT = particle.lifeProgress(); + float flashCenter = 0.34f + ColorEffects.hash01(particle.visualPhase() + 5.73f) * 0.38f; + float flash = 1.0f - AnimationUtility.smoothstep(Mth.clamp(Math.abs(lifeT - flashCenter) / 0.13f, 0.0f, 1.0f)); + if (flash <= 0.002f) return; + int flashArgb = funnelColor(baseColor, 0x7FE8FF, alpha * FLASH_ALPHA_MULTIPLIER * flash); + addQuad(mesh, pos, camRot, particle.size() * (1.45f + flash * 0.82f) * scale, particle.visualPhase() * 0.21f, flashArgb); + } + + private static void renderLeaks(MeshBuilder mesh, WorldParticlesParticle particle, Vec3 pos, + Quaternionf camRot, float alpha, float scale, int baseColor, float ageSeconds) { + for (int i = 0; i < 4; i++) { + float seed = ColorEffects.hash01(particle.visualPhase() + i * 13.17f); + float angle = particle.visualPhase() + i * 1.5708f + ageSeconds * (0.28f + seed * 0.16f); + float distance = particle.size() * scale * (1.45f + seed * 1.10f); + Vec3 leakPos = WorldParticlesGeometry.offsetInBillboardPlane(pos, camRot, angle, distance); + + float wave = 0.5f + 0.5f * (float) Math.sin(ageSeconds * (2.0f + seed * 0.9f) + particle.visualPhase() + i); + float leakAlpha = alpha * LEAK_ALPHA_MULTIPLIER * (0.45f + AnimationUtility.smoothstep(wave) * 0.55f); + int leakRgb = switch (i & 3) { + case 0 -> 0xFF3A24; + case 1 -> 0xA7FF2E; + case 2 -> 0xFFD84E; + default -> 0x55F7FF; + }; + + WorldParticlesGeometry.addBillboardRect( + mesh, + leakPos.x, leakPos.y, leakPos.z, + particle.size() * scale * (0.055f + seed * 0.035f), + particle.size() * scale * (0.42f + seed * 0.34f), + camRot, + angle - 1.5708f, + funnelColor(baseColor, leakRgb, leakAlpha) + ); + } + } + + private static void addQuad(MeshBuilder mesh, Vec3 pos, Quaternionf camRot, float size, float roll, int argb) { + WorldParticlesGeometry.addBillboardQuad(mesh, pos.x, pos.y, pos.z, size, camRot, roll, argb); + } + + private static int funnelColor(int baseArgb, int sourceRgb, float alphaMultiplier) { + return ColorEffects.argbWithBaseAlpha(baseArgb, sourceRgb, alphaMultiplier); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/HitBubbles.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/HitBubbles.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/HitBubbles.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/HitBubbles.java diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/HitEffect.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/HitEffect.java new file mode 100644 index 0000000..64482e1 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/HitEffect.java @@ -0,0 +1,120 @@ +package dev.loki.lovisual.features.module.modules.visuals.effects.hits; + +import dev.loki.lovisual.config.values.color.RGBAColorValue; +import dev.loki.lovisual.config.values.mode.EnumValue; +import dev.loki.lovisual.config.values.mode.ModeValue; +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import dev.loki.lovisual.features.module.core.Module; +import dev.loki.lovisual.features.module.core.ModuleCategory; +import dev.loki.lovisual.features.module.core.ModuleInfo; +import dev.loki.lovisual.features.module.phase.WorldPhase; +import dev.loki.lovisual.render.engine.animation.AnimatedColorModeOptions; +import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors; +import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import dev.loki.lovisual.render.helpers.util.ParticleTextureMode; +import net.minecraft.client.Minecraft; +import net.minecraft.core.BlockPos; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.phys.Vec3; + +@ModuleInfo(id = "hiteffect", displayName = "HitEffect", category = ModuleCategory.VISUALS) +public class HitEffect extends Module { + private static final String SETTING_EFFECT_MODE = "effect_mode"; + private static final String SETTING_LIFE_TIME = "life_time"; + private static final String SETTING_SCALE = "scale"; + private static final String SETTING_ONLY_PLAYERS = "only_players"; + private static final String SETTING_DEPTH_TEST = "depth_test"; + private static final String SETTING_STARS = "stars"; + private static final String SETTING_STARS_COLOR = "stars_color"; + private static final String SETTING_CRIT_STARS_COLOR = "crit_stars_color"; + private static final String SETTING_COLOR_MODE = "color_mode"; + private static final String SETTING_COLOR_SPEED = "color_speed"; + private static final String SETTING_COLOR = "color"; + private static final String SETTING_COLOR2 = "color2"; + private static final String SETTING_WAVE_RADIUS = "wave_radius"; + private static final String SETTING_WAVE_DURATION = "wave_duration"; + private static final String SETTING_IMPACT_RADIUS = "impact_radius"; + private static final String SETTING_IMPACT_DURATION = "impact_duration"; + private static final String SETTING_IMPACT_GLOW_HEIGHT = "impact_glow_height"; + private static final String SETTING_HIT_PARTICLES = "hit_particles"; + private static final String SETTING_HIT_PARTICLE_COUNT = "hit_particle_count"; + private static final String SETTING_HIT_PARTICLE_SIZE = "hit_particle_size"; + private static final String SETTING_HIT_PARTICLE_LIFE = "hit_particle_life_ms"; + private static final String SETTING_HIT_PARTICLE_TEXTURE = "hit_particle_texture"; + private static final String SETTING_HIT_PARTICLE_RANDOM_COLOR = "hit_particle_random_color"; + final EnumValue effectMode = enumSetting("hitBubblesEffectMode", SETTING_EFFECT_MODE, EffectMode.BUBBLES, EffectMode.values()); + final NumberValue lifeTime = visibleWhen(num("hitBubblesLifeTime", SETTING_LIFE_TIME, 30, 1, 150), this::isBubblesMode); + final NumberValue bubbleScale = visibleWhen(num("hitBubblesScale", SETTING_SCALE, 1.0f, 0.4f, 2.0f), this::isBubblesMode); + final ModeValue colorMode = modeSetting("hitBubblesColorMode", SETTING_COLOR_MODE, "Static", "Static", "Rainbow", "LightRainbow", "Sky", "Fade", "DoubleColor", "Analogous", "Theme"); + final NumberValue colorSpeed = num("hitBubblesColorSpeed", SETTING_COLOR_SPEED, 18, 2, 54); + final RGBAColorValue colorValue = color("hitBubblesColor", SETTING_COLOR, "#FF55FFFF"); + final RGBAColorValue colorValue2 = visibleWhen(color("hitBubblesColor2", SETTING_COLOR2, "#FFFF5555"), this::usesSecondaryColor); + final BooleanValue depthTest = bool("hitBubblesDepthTest", SETTING_DEPTH_TEST, true); + final BooleanValue onlyPlayers = bool("hitBubblesOnlyPlayers", SETTING_ONLY_PLAYERS, true); + final BooleanValue starsEnabled = visibleWhen(bool("hitBubblesStarsEnabled", SETTING_STARS, true), this::isBubblesMode); + final RGBAColorValue starsColor = visibleWhen(color("hitBubblesStarsColor", SETTING_STARS_COLOR, "#FFFFE16B"), () -> isBubblesMode() && starsEnabled.get()); + final RGBAColorValue critStarsColor = visibleWhen(color("hitBubblesCritStarsColor", SETTING_CRIT_STARS_COLOR, "#FFFF3A2E"), () -> isBubblesMode() && starsEnabled.get()); + final NumberValue waveRadius = visibleWhen(num("hitBubblesWaveRadius", SETTING_WAVE_RADIUS, 12, 3, 32), this::isWaveMode); + final NumberValue waveDurationMs = visibleWhen(num("hitBubblesWaveDuration", SETTING_WAVE_DURATION, 1500, 200, 6000), this::isWaveMode); + final NumberValue impactRadius = visibleWhen(num("hitBubblesImpactRadius", SETTING_IMPACT_RADIUS, 1.4f, 0.4f, 4.0f), this::isImpactMode); + final NumberValue impactDurationMs = visibleWhen(num("hitBubblesImpactDuration", SETTING_IMPACT_DURATION, 450, 150, 1500), this::isImpactMode); + final NumberValue impactGlowHeight = visibleWhen(num("hitBubblesImpactGlowHeight", SETTING_IMPACT_GLOW_HEIGHT, 0.8f, 0.2f, 2.5f), this::isImpactMode); + final BooleanValue hitParticlesEnabled = bool("hitEffectParticlesEnabled", SETTING_HIT_PARTICLES, false); + final NumberValue hitParticleCount = visibleWhen(num("hitEffectParticleCount", SETTING_HIT_PARTICLE_COUNT, 15, 1, 80), hitParticlesEnabled::get); + final NumberValue hitParticleSize = visibleWhen(num("hitEffectParticleSize", SETTING_HIT_PARTICLE_SIZE, 0.12f, 0.04f, 0.4f), hitParticlesEnabled::get); + final NumberValue hitParticleLifeMs = visibleWhen(num("hitEffectParticleLifeMs", SETTING_HIT_PARTICLE_LIFE, 2200, 200, 6000), hitParticlesEnabled::get); + final EnumValue hitParticleTexture = visibleWhen(enumSetting("hitEffectParticleTexture", SETTING_HIT_PARTICLE_TEXTURE, ParticleTextureMode.BLOOM, ParticleTextureMode.values()), hitParticlesEnabled::get); + final BooleanValue hitParticleRandomColor = visibleWhen(bool("hitEffectParticleRandomColor", SETTING_HIT_PARTICLE_RANDOM_COLOR, false), hitParticlesEnabled::get); + private final HitBubbleHelper bubbleHelper; + private final HitParticleHelper particleHelper; + private final HitWaveImpactHelper waveImpactHelper; + + public HitEffect() { + bubbleHelper = new HitBubbleHelper(this); + particleHelper = new HitParticleHelper(this); + waveImpactHelper = new HitWaveImpactHelper(this); + } + + @Override public WorldPhase getWorldPhase() { return WorldPhase.END_MAIN; } + @Override public void onDisable() { bubbleHelper.clear(); particleHelper.clear(); waveImpactHelper.clear(); } + public void handleHit(Entity target) { + Minecraft mc = Minecraft.getInstance(); + if (!isEnabled() || mc.player == null || mc.level == null) return; + if (!(target instanceof LivingEntity)) return; + if (onlyPlayers.get() && !(target instanceof Player)) return; + if (effectMode.get() == EffectMode.WAVE) { + Vec3 pos = target.position(); + BlockPos basePos = BlockPos.containing(pos.x, pos.y - 0.1, pos.z); + waveImpactHelper.addWave(basePos); + } else if (effectMode.get() == EffectMode.IMPACT) { + waveImpactHelper.addImpact(target.position()); + } else { + bubbleHelper.handleHit((LivingEntity) target); + } + if (hitParticlesEnabled.get()) particleHelper.spawn((LivingEntity) target); + } + @Override public void onTick() { + Minecraft mc = Minecraft.getInstance(); + if (!isEnabled() || mc.player == null || mc.level == null) return; + bubbleHelper.tick(); + particleHelper.tick(); + } + @Override public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); + if (!isEnabled() || mc.player == null || mc.level == null) return; + if (hitParticlesEnabled.get()) particleHelper.render(renderer, tickDelta); + if (effectMode.get() == EffectMode.WAVE) { waveImpactHelper.renderWave(renderer); return; } + if (effectMode.get() == EffectMode.IMPACT) { waveImpactHelper.renderImpact(renderer); return; } + bubbleHelper.render(renderer, tickDelta); + } + boolean isBubblesMode() { return effectMode.get() == EffectMode.BUBBLES; } + boolean isWaveMode() { return effectMode.get() == EffectMode.WAVE; } + boolean isImpactMode() { return effectMode.get() == EffectMode.IMPACT; } + boolean usesSecondaryColor() { return AnimatedRenderColors.usesSecondary(animatedColorMode()); } + int getColorArgb(int count) { return AnimatedRenderColors.resolve(animatedColorMode(), colorSpeed.get(), count, colorValue.getArgb(), colorValue2.getArgb()); } + private AnimatedRenderColors.Mode animatedColorMode() { return AnimatedColorModeOptions.parse(colorMode.get()); } + public enum EffectMode { BUBBLES, WAVE, IMPACT } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/KillEffect.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/KillEffect.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/KillEffect.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/KillEffect.java diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/effects/HitBubbleHelper.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/effects/HitBubbleHelper.java new file mode 100644 index 0000000..0074ff6 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/effects/HitBubbleHelper.java @@ -0,0 +1,168 @@ +package dev.loki.lovisual.features.module.modules.visuals.effects.hits; + +import com.mojang.blaze3d.pipeline.RenderPipeline; +import dev.loki.lovisual.mixininterface.entity.ILocalPlayer; +import dev.loki.lovisual.mixininterface.entity.IPlayerAttackCooldown; +import dev.loki.lovisual.render.engine.RenderState; +import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; +import dev.loki.lovisual.render.engine.texture.TextureStorage; +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; +import dev.loki.lovisual.render.helpers.particle.OrientedQuadRenderer; +import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import dev.loki.lovisual.util.combat.hit.AttackHitPoint; +import dev.loki.lovisual.util.time.Timer; +import net.minecraft.client.Minecraft; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.phys.Vec3; +import org.joml.Quaternionf; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +final class HitBubbleHelper { + private static final int NORMAL_STARS = 12; + private static final int CRIT_STARS = 18; + private static final float CHARGED_THRESHOLD = 0.9f; + + private final HitEffect parent; + private final Random random = new Random(); + private final List bubbles = new ArrayList<>(); + private final List stars = new ArrayList<>(); + + HitBubbleHelper(HitEffect parent) { + this.parent = parent; + } + + void clear() { + bubbles.clear(); + stars.clear(); + } + + void handleHit(LivingEntity target) { + Minecraft mc = Minecraft.getInstance(); + if (mc.player == null) return; + ILocalPlayer access = (ILocalPlayer) mc.player; + float yaw = access.lovisual$getLastYaw(); + float pitch = access.lovisual$getLastPitch(); + Vec3 point = AttackHitPoint.resolve(target, yaw, pitch); + if (point == null) point = target.position(); + bubbles.add(new HitBubble(point, -yaw, pitch, new Timer())); + if (!parent.starsEnabled.get()) return; + if (!(mc.player instanceof IPlayerAttackCooldown cd)) return; + float cooldown = cd.lovisual$getAttackCooldownProgress(0.0f); + if (cooldown < CHARGED_THRESHOLD) return; + boolean crit = isCritical(mc.player, cooldown); + spawnStars(point, crit); + } + + void tick() { + if (!parent.isBubblesMode() || !parent.starsEnabled.get()) { + stars.clear(); + return; + } + if (!stars.isEmpty()) stars.removeIf(Star::update); + } + + void render(Renderer3D renderer, float tickDelta) { + boolean useDepth = parent.depthTest.get(); + RenderPipeline pipeline = useDepth + ? LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE_LIQUID_IGNORE + : LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE; + Renderer3D.DepthMode depthMode = useDepth ? Renderer3D.DepthMode.PRE_DEPTH : Renderer3D.DepthMode.MAIN; + if (!bubbles.isEmpty()) { + MeshBuilder mesh = renderer.batchTextured(pipeline, TextureStorage.BUBBLE, depthMode); + if (mesh != null) { + for (HitBubble b : bubbles) { + float lifeMs = b.life.getPassedTimeMs(); + float lifeTotal = Math.max(1f, parent.lifeTime.get() * 50f); + float angle = -lifeMs / 4f; + float factor = Math.min(1f, lifeMs / lifeTotal); + float scale = factor * 2f * parent.bubbleScale.get(); + if (scale <= 0.01f) continue; + int c1 = HitRenderUtil.applyOpacity(parent.getColorArgb(270), 1f - factor); + int c2 = HitRenderUtil.applyOpacity(parent.getColorArgb(0), 1f - factor); + int c3 = HitRenderUtil.applyOpacity(parent.getColorArgb(180), 1f - factor); + int c4 = HitRenderUtil.applyOpacity(parent.getColorArgb(90), 1f - factor); + OrientedQuadRenderer.addOrientedQuad(mesh, b.pos, b.yaw, b.pitch, angle, scale, c1, c2, c3, c4); + } + bubbles.removeIf(b -> b.life.passedMs(parent.lifeTime.get() * 50L)); + } + } + if (parent.starsEnabled.get() && !stars.isEmpty()) { + MeshBuilder mesh = renderer.batchTextured(pipeline, TextureStorage.PARTICLE_STARS, depthMode); + if (mesh == null) return; + Quaternionf camRot = RenderState.cameraRotation; + for (Star s : stars) { + Vec3 pos = s.interpolate(tickDelta); + float t = s.getLifeProgress(); + float scale = s.size * (1f - t); + if (scale <= 0.01f) continue; + int argb = HitRenderUtil.applyOpacity(s.color, 1f - t); + HitRenderUtil.addBillboardQuad(mesh, pos.x, pos.y, pos.z, scale, camRot, argb); + } + } + } + + private boolean isCritical(Player attacker, float cooldown) { + if (cooldown < CHARGED_THRESHOLD) return false; + if (attacker.onGround()) return false; + if (attacker.onClimbable()) return false; + if (attacker.isInWater()) return false; + if (attacker.isPassenger()) return false; + return attacker.fallDistance > 0.0f; + } + + private void spawnStars(Vec3 pos, boolean crit) { + int count = crit ? CRIT_STARS : NORMAL_STARS; + int color = crit ? parent.critStarsColor.getArgb() : parent.starsColor.getArgb(); + for (int i = 0; i < count; i++) { + double angle = random.nextDouble() * Math.PI * 2.0; + double speed = 0.14 + random.nextDouble() * 0.14; + double vx = Math.cos(angle) * speed; + double vz = Math.sin(angle) * speed; + double vy = 0.12 + random.nextDouble() * 0.12; + long life = 500 + random.nextInt(250); + float size = 0.18f + random.nextFloat() * 0.12f; + stars.add(new Star(pos, new Vec3(vx, vy, vz), life, size, color)); + } + } + + private record HitBubble(Vec3 pos, float yaw, float pitch, Timer life) {} + + private static class Star { + private final Timer timer; + private final long lifeMs; + private final float size; + private final int color; + private Vec3 pos; + private Vec3 prevPos; + private Vec3 velocity; + + private Star(Vec3 pos, Vec3 velocity, long lifeMs, float size, int color) { + this.pos = pos; + this.prevPos = pos; + this.velocity = velocity; + this.timer = new Timer(); + this.lifeMs = lifeMs; + this.size = size; + this.color = color; + } + + private boolean update() { + prevPos = pos; + pos = pos.add(velocity); + velocity = velocity.multiply(0.94, 0.94, 0.94).add(0.0, -0.03, 0.0); + return timer.passedMs(lifeMs); + } + + private Vec3 interpolate(float tickDelta) { + return prevPos.lerp(pos, tickDelta); + } + + private float getLifeProgress() { + return Math.min(1f, timer.getPassedTimeMs() / (float) lifeMs); + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/effects/HitParticleHelper.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/effects/HitParticleHelper.java new file mode 100644 index 0000000..b474313 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/effects/HitParticleHelper.java @@ -0,0 +1,153 @@ +package dev.loki.lovisual.features.module.modules.visuals.effects.hits; + +import com.mojang.blaze3d.pipeline.RenderPipeline; +import dev.loki.lovisual.features.module.modules.visuals.optimization.FpsOptimizer; +import dev.loki.lovisual.render.engine.RenderState; +import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; +import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import dev.loki.lovisual.render.engine.texture.TextureStorage; +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; +import dev.loki.lovisual.render.helpers.player.Particle3D; +import dev.loki.lovisual.render.helpers.util.ParticleTextureMode; +import net.minecraft.client.Minecraft; +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.phys.Vec3; +import org.joml.Quaternionf; + +import java.awt.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +final class HitParticleHelper { + private static final float HIT_PARTICLE_GRAVITY = 0.02f; + private static final float HIT_PARTICLE_DRAG = 0.94f; + private static final int HIT_PARTICLE_MAX = 320; + private static final int HIT_PARTICLE_SPAWN_CAP_PER_TICK = 32; + + private final HitEffect parent; + private final Random random = new Random(); + private final List hitParticles = new ArrayList<>(); + private final List[] hitParticleBuckets = createBuckets(); + private int lastHitParticleTick = Integer.MIN_VALUE; + private int spawnedHitParticlesThisTick = 0; + + HitParticleHelper(HitEffect parent) { + this.parent = parent; + } + + void clear() { + hitParticles.clear(); + lastHitParticleTick = Integer.MIN_VALUE; + spawnedHitParticlesThisTick = 0; + } + + void tick() { + if (!hitParticles.isEmpty()) hitParticles.removeIf(Particle3D::update); + } + + void spawn(LivingEntity target) { + int count = reserveHitParticleBudget(Math.max(0, parent.hitParticleCount.get())); + if (count <= 0) return; + int baseColor = parent.getColorArgb(target.getId() % 360); + float size = parent.hitParticleSize.get(); + long life = parent.hitParticleLifeMs.get(); + long fadeIn = Math.min(250L, life / 4L); + long fadeOut = Math.min(900L, life / 2L); + for (int i = 0; i < count; i++) { + if (hitParticles.size() >= HIT_PARTICLE_MAX) hitParticles.remove(0); + double px = target.getX() + random.nextDouble() * 0.6 - 0.3; + double py = target.getY() + random.nextDouble() * Math.max(0.2, target.getBbHeight()); + double pz = target.getZ() + random.nextDouble() * 0.6 - 0.3; + double motion = 0.06; + Vec3 vel = new Vec3(random.nextDouble() * motion * 2 - motion, + random.nextDouble() * 0.04 - 0.02, + random.nextDouble() * motion * 2 - motion); + int color = parent.hitParticleRandomColor.get() ? randomColor() : baseColor; + ParticleTexture tex = resolveParticleTexture(); + hitParticles.add(new Particle3D(new Vec3(px, py, pz), vel, life, fadeIn, fadeOut, size, color, + tex.texture(), tex.index(), HIT_PARTICLE_DRAG, HIT_PARTICLE_GRAVITY, + (random.nextFloat() - 0.5f) * 8f)); + } + } + + void render(Renderer3D renderer, float tickDelta) { + if (hitParticles.isEmpty()) return; + boolean useDepth = parent.depthTest.get(); + RenderPipeline pipeline = useDepth ? LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE_LIQUID_IGNORE + : LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE; + Renderer3D.DepthMode depthMode = useDepth ? Renderer3D.DepthMode.PRE_DEPTH : Renderer3D.DepthMode.MAIN; + Quaternionf camRot = RenderState.cameraRotation; + if (parent.hitParticleTexture.get() == ParticleTextureMode.BLOOM) { + if (FpsOptimizer.bloomDisabled()) return; + MeshBuilder mesh = renderer.batchTextured(pipeline, TextureStorage.FIRE_FLY, depthMode); + if (mesh == null) return; + for (Particle3D p : hitParticles) renderParticle(mesh, p, tickDelta, camRot); + return; + } + clearBuckets(hitParticleBuckets); + for (Particle3D p : hitParticles) { + int idx = p.textureIndex(); + if (idx >= 0 && idx < hitParticleBuckets.length) hitParticleBuckets[idx].add(p); + } + for (int i = 0; i < hitParticleBuckets.length; i++) { + List bucket = hitParticleBuckets[i]; + if (bucket.isEmpty()) continue; + MeshBuilder mesh = renderer.batchTextured(pipeline, TextureStorage.RANDOM_PARTICLES[i], depthMode); + if (mesh == null) continue; + for (Particle3D p : bucket) renderParticle(mesh, p, tickDelta, camRot); + } + } + + private void renderParticle(MeshBuilder mesh, Particle3D particle, float tickDelta, Quaternionf camRot) { + Vec3 pos = particle.interpolate(tickDelta); + float alpha = particle.alpha(); + if (alpha <= 0.01f) return; + int argb = HitRenderUtil.applyOpacity(particle.color(), alpha); + HitRenderUtil.addBillboardQuad(mesh, pos.x, pos.y, pos.z, particle.size(), camRot, argb); + } + + private int reserveHitParticleBudget(int requested) { + Minecraft mc = Minecraft.getInstance(); + if (requested <= 0 || mc.player == null) return 0; + int tick = mc.player.tickCount; + if (tick != lastHitParticleTick) { + lastHitParticleTick = tick; + spawnedHitParticlesThisTick = 0; + } + int remaining = HIT_PARTICLE_SPAWN_CAP_PER_TICK - spawnedHitParticlesThisTick; + if (remaining <= 0) return 0; + int granted = Math.min(requested, remaining); + spawnedHitParticlesThisTick += granted; + return granted; + } + + private int randomColor() { + float h = random.nextFloat(); + float s = 0.7f + random.nextFloat() * 0.3f; + float v = 0.8f + random.nextFloat() * 0.2f; + return Color.HSBtoRGB(h, s, v) | 0xFF000000; + } + + private ParticleTexture resolveParticleTexture() { + if (parent.hitParticleTexture.get() == ParticleTextureMode.BLOOM) { + return new ParticleTexture(TextureStorage.FIRE_FLY, -1); + } + int idx = random.nextInt(TextureStorage.RANDOM_PARTICLES.length); + return new ParticleTexture(TextureStorage.RANDOM_PARTICLES[idx], idx); + } + + @SuppressWarnings("unchecked") + private static List[] createBuckets() { + List[] buckets = new List[TextureStorage.RANDOM_PARTICLES.length]; + for (int i = 0; i < buckets.length; i++) buckets[i] = new ArrayList<>(); + return buckets; + } + + private static void clearBuckets(List[] buckets) { + for (List bucket : buckets) bucket.clear(); + } + + private record ParticleTexture(Identifier texture, int index) {} +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/render/HitRenderUtil.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/render/HitRenderUtil.java new file mode 100644 index 0000000..0637c80 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/render/HitRenderUtil.java @@ -0,0 +1,74 @@ +package dev.loki.lovisual.features.module.modules.visuals.effects.hits; + +import net.minecraft.core.BlockPos; +import net.minecraft.util.Mth; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.joml.Quaternionf; +import org.joml.Vector3f; +import dev.loki.lovisual.render.engine.color.RenderColor; +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; + +final class HitRenderUtil { + private HitRenderUtil() {} + + static int applyOpacity(int argb, float opacity) { + opacity = Math.min(1f, Math.max(0f, opacity)); + int a = (argb >>> 24) & 0xFF; + int na = Mth.clamp((int) (a * opacity), 0, 255); + return (na << 24) | (argb & 0x00FFFFFF); + } + + static void addBillboardQuad(MeshBuilder mesh, double cx, double cy, double cz, + float size, Quaternionf camRot, int argb) { + Vector3f right = new Vector3f(1, 0, 0).rotate(camRot).mul(size); + Vector3f up = new Vector3f(0, 1, 0).rotate(camRot).mul(size); + double p1x = cx - right.x() - up.x(); + double p1y = cy - right.y() - up.y(); + double p1z = cz - right.z() - up.z(); + double p2x = cx + right.x() - up.x(); + double p2y = cy + right.y() - up.y(); + double p2z = cz + right.z() - up.z(); + double p3x = cx + right.x() + up.x(); + double p3y = cy + right.y() + up.y(); + double p3z = cz + right.z() + up.z(); + double p4x = cx - right.x() + up.x(); + double p4y = cy - right.y() + up.y(); + double p4z = cz - right.z() + up.z(); + mesh.ensureQuadCapacity(); + int i1 = mesh.vec3(p1x, p1y, p1z).vec2(0, 1).color(new RenderColor(argb)).next(); + int i2 = mesh.vec3(p2x, p2y, p2z).vec2(1, 1).color(new RenderColor(argb)).next(); + int i3 = mesh.vec3(p3x, p3y, p3z).vec2(1, 0).color(new RenderColor(argb)).next(); + int i4 = mesh.vec3(p4x, p4y, p4z).vec2(0, 0).color(new RenderColor(argb)).next(); + mesh.quad(i1, i2, i3, i4); + } + + static void addShapeOutline(MeshBuilder mesh, BlockPos pos, VoxelShape shape, int argb, double eps) { + if (mesh == null || shape == null) return; + int a = (argb >>> 24) & 0xFF; + int r = (argb >>> 16) & 0xFF; + int g = (argb >>> 8) & 0xFF; + int b = argb & 0xFF; + VoxelShape offset = shape.move(pos.getX(), pos.getY(), pos.getZ()); + offset.forAllEdges((x1, y1, z1, x2, y2, z2) -> { + double cx = pos.getX() + 0.5; + double cy = pos.getY() + 0.5; + double cz = pos.getZ() + 0.5; + double sx1 = Math.signum(x1 - cx); + double sy1 = Math.signum(y1 - cy); + double sz1 = Math.signum(z1 - cz); + double sx2 = Math.signum(x2 - cx); + double sy2 = Math.signum(y2 - cy); + double sz2 = Math.signum(z2 - cz); + double ax1 = x1 + sx1 * eps; + double ay1 = y1 + sy1 * eps; + double az1 = z1 + sz1 * eps; + double ax2 = x2 + sx2 * eps; + double ay2 = y2 + sy2 * eps; + double az2 = z2 + sz2 * eps; + mesh.ensureLineCapacity(); + int i1 = mesh.vec3(ax1, ay1, az1).color(r, g, b, a).next(); + int i2 = mesh.vec3(ax2, ay2, az2).color(r, g, b, a).next(); + mesh.line(i1, i2); + }); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/render/HitWaveImpactHelper.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/render/HitWaveImpactHelper.java new file mode 100644 index 0000000..95ca384 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/render/HitWaveImpactHelper.java @@ -0,0 +1,176 @@ +package dev.loki.lovisual.features.module.modules.visuals.effects.hits; + +import com.mojang.blaze3d.pipeline.RenderPipeline; +import dev.loki.lovisual.render.engine.RenderState; +import dev.loki.lovisual.render.engine.color.RenderColor; +import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; +import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; +import dev.loki.lovisual.util.time.GameClock; +import net.minecraft.client.Minecraft; +import net.minecraft.core.BlockPos; +import net.minecraft.util.Mth; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.phys.shapes.VoxelShape; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +final class HitWaveImpactHelper { + private static final float WAVE_WIDTH = 2.5f; + private static final int WAVE_MAX_PER_FRAME = 400; + private static final float WAVE_MIN_ALPHA = 0.02f; + private static final double WAVE_OUTLINE_EPS = 0.002; + private static final float WAVE_LINE_WIDTH_BASE = 0.8f; + private static final float WAVE_LINE_WIDTH_BOOST = 0.6f; + private static final int IMPACT_RING_SEGMENTS = 24; + private final HitEffect parent; + private final List waves = new ArrayList<>(); + private final List impacts = new ArrayList<>(); + + HitWaveImpactHelper(HitEffect parent) { this.parent = parent; } + void clear() { waves.clear(); impacts.clear(); } + void addWave(BlockPos pos) { + Minecraft mc = Minecraft.getInstance(); + if (mc.level != null) waves.add(new WaveEffect(pos, GameClock.millis())); + } + void addImpact(Vec3 center) { impacts.add(new ImpactRing(center, GameClock.millis())); } + void renderWave(Renderer3D renderer) { + if (waves.isEmpty()) return; + Iterator it = waves.iterator(); + while (it.hasNext()) { + WaveEffect w = it.next(); + if (w.isExpired()) { it.remove(); continue; } + w.render(renderer); + } + } + void renderImpact(Renderer3D renderer) { + if (impacts.isEmpty()) return; + Iterator it = impacts.iterator(); + while (it.hasNext()) { + ImpactRing r = it.next(); + if (r.isExpired()) { it.remove(); continue; } + r.render(renderer); + } + } + private class WaveEffect { + private final BlockPos centerPos; + private final long startTime; + WaveEffect(BlockPos centerPos, long startTime) { this.centerPos = centerPos; this.startTime = startTime; } + boolean isExpired() { return GameClock.millis() - startTime > Math.max(1L, parent.waveDurationMs.get()); } + void render(Renderer3D renderer) { + Minecraft mc = Minecraft.getInstance(); + if (mc.level == null || renderer == null) return; + long durationMs = Math.max(1L, parent.waveDurationMs.get()); + int maxRadius = Math.max(1, parent.waveRadius.get()); + long elapsed = GameClock.millis() - startTime; + float progress = Mth.clamp((float) elapsed / durationMs, 0f, 1f); + float currentRadius = progress * maxRadius; + float globalAlpha = (float) Math.pow(1.0f - progress, 0.6); + float minRadSq = (currentRadius - WAVE_WIDTH) * (currentRadius - WAVE_WIDTH); + float maxRadSq = (currentRadius + 0.5f) * (currentRadius + 0.5f); + float prevWidth = RenderState.lineWidth; + RenderState.lineWidth = Math.max(0.5f, WAVE_LINE_WIDTH_BASE + globalAlpha * WAVE_LINE_WIDTH_BOOST); + try { + boolean useDepth = parent.depthTest.get(); + RenderPipeline pipeline = useDepth ? LoVisualRenderPipelines.WORLD_COLORED_LINES_LIQUID_IGNORE : LoVisualRenderPipelines.WORLD_COLORED_LINES; + Renderer3D.DepthMode depthMode = useDepth ? Renderer3D.DepthMode.PRE_DEPTH : Renderer3D.DepthMode.MAIN; + MeshBuilder mesh = renderer.batch(pipeline, depthMode); + if (mesh == null) return; + int rendered = 0; + for (int x = -maxRadius; x <= maxRadius; x++) { + for (int z = -maxRadius; z <= maxRadius; z++) { + if (rendered >= WAVE_MAX_PER_FRAME) return; + float distSq = x * x + z * z; + if (distSq < minRadSq || distSq > maxRadSq) continue; + BlockPos checkPos = centerPos.offset(x, 0, z); + BlockPos renderPos = findSurface(checkPos); + if (renderPos == null) continue; + BlockState state = mc.level.getBlockState(renderPos); + VoxelShape shape = state.getShape(mc.level, renderPos); + if (shape.isEmpty()) continue; + rendered++; + float distance = (float) Math.sqrt(distSq); + float localAlpha = 1.0f - Math.abs(distance - currentRadius) / WAVE_WIDTH; + localAlpha = Math.max(0f, Math.min(1f, localAlpha)) * globalAlpha; + if (localAlpha <= WAVE_MIN_ALPHA) continue; + int colorIndex = (int) (Math.toDegrees(Math.atan2(z, x)) + 180.0); + int color = HitRenderUtil.applyOpacity(parent.getColorArgb(colorIndex), localAlpha); + HitRenderUtil.addShapeOutline(mesh, renderPos, shape, color, WAVE_OUTLINE_EPS); + } + } + } finally { RenderState.lineWidth = prevWidth; } + } + private BlockPos findSurface(BlockPos pos) { + Minecraft mc = Minecraft.getInstance(); + for (int y = 2; y >= -4; y--) { + BlockPos p = pos.above(y); + if (!mc.level.getBlockState(p).isAir() && mc.level.getBlockState(p.above()).isAir()) return p; + } + return null; + } + } + private class ImpactRing { + private final Vec3 center; + private final long startTime; + ImpactRing(Vec3 center, long startTime) { this.center = center; this.startTime = startTime; } + boolean isExpired() { return GameClock.millis() - startTime > Math.max(1L, parent.impactDurationMs.get()); } + void render(Renderer3D renderer) { + long durationMs = Math.max(1L, parent.impactDurationMs.get()); + long elapsed = GameClock.millis() - startTime; + float progress = Mth.clamp((float) elapsed / durationMs, 0f, 1f); + float fade = (float) Math.pow(1.0 - progress, 1.4); + if (fade <= 0.01f) return; + float maxRadius = Math.max(0.1f, parent.impactRadius.get()); + float radius = maxRadius * (0.15f + 0.85f * progress); + int color = parent.getColorArgb((int) (progress * 360)); + MeshBuilder ring = renderer.batch(LoVisualRenderPipelines.WORLD_COLORED_ADDITIVE, Renderer3D.DepthMode.MAIN); + if (ring != null) renderRing(ring, radius, fade, color); + MeshBuilder glow = renderer.batch(LoVisualRenderPipelines.WORLD_COLORED_ADDITIVE_DEPTH, Renderer3D.DepthMode.PRE_DEPTH); + if (glow != null) renderGlowWall(glow, radius, fade * 0.6f, color); + } + private void renderRing(MeshBuilder mesh, float radius, float fade, int color) { + float inner = radius * 0.72f; + for (int i = 0; i < IMPACT_RING_SEGMENTS; i++) { + double a0 = (Math.PI * 2.0 / IMPACT_RING_SEGMENTS) * i; + double a1 = (Math.PI * 2.0 / IMPACT_RING_SEGMENTS) * (i + 1); + float ix0 = (float) (Math.cos(a0) * inner); + float iz0 = (float) (Math.sin(a0) * inner); + float ix1 = (float) (Math.cos(a1) * inner); + float iz1 = (float) (Math.sin(a1) * inner); + float ox0 = (float) (Math.cos(a0) * radius); + float oz0 = (float) (Math.sin(a0) * radius); + float ox1 = (float) (Math.cos(a1) * radius); + float oz1 = (float) (Math.sin(a1) * radius); + int edgeColor = HitRenderUtil.applyOpacity(color, fade); + int fadeColor = HitRenderUtil.applyOpacity(color, 0f); + mesh.ensureQuadCapacity(); + int i1 = mesh.vec3(center.x + ix0, center.y + 0.02, center.z + iz0).color(new RenderColor(fadeColor)).next(); + int i2 = mesh.vec3(center.x + ix1, center.y + 0.02, center.z + iz1).color(new RenderColor(fadeColor)).next(); + int i3 = mesh.vec3(center.x + ox1, center.y + 0.02, center.z + oz1).color(new RenderColor(edgeColor)).next(); + int i4 = mesh.vec3(center.x + ox0, center.y + 0.02, center.z + oz0).color(new RenderColor(edgeColor)).next(); + mesh.quad(i1, i2, i3, i4); + } + } + private void renderGlowWall(MeshBuilder mesh, float radius, float fade, int color) { + float height = parent.impactGlowHeight.get() * (1.0f - fade * 0.3f); + for (int i = 0; i < IMPACT_RING_SEGMENTS; i++) { + double a0 = (Math.PI * 2.0 / IMPACT_RING_SEGMENTS) * i; + double a1 = (Math.PI * 2.0 / IMPACT_RING_SEGMENTS) * (i + 1); + float x0 = (float) (Math.cos(a0) * radius); + float z0 = (float) (Math.sin(a0) * radius); + float x1 = (float) (Math.cos(a1) * radius); + float z1 = (float) (Math.sin(a1) * radius); + int baseColor = HitRenderUtil.applyOpacity(color, fade); + int topColor = HitRenderUtil.applyOpacity(color, 0f); + mesh.ensureQuadCapacity(); + int i1 = mesh.vec3(center.x + x0, center.y, center.z + z0).color(new RenderColor(baseColor)).next(); + int i2 = mesh.vec3(center.x + x1, center.y, center.z + z1).color(new RenderColor(baseColor)).next(); + int i3 = mesh.vec3(center.x + x1, center.y + height, center.z + z1).color(new RenderColor(topColor)).next(); + int i4 = mesh.vec3(center.x + x0, center.y + height, center.z + z0).color(new RenderColor(topColor)).next(); + mesh.quad(i1, i2, i3, i4); + } + } + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/ESP.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/ESP.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/ESP.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/ESP.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/EspColorUtil.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/EspColorUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/EspColorUtil.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/EspColorUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/EspEntityCollector.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/EspEntityCollector.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/EspEntityCollector.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/EspEntityCollector.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/EspModel.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/EspModel.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/EspModel.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/EspModel.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/audio/LightningTargetESP.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/audio/LightningTargetESP.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/audio/LightningTargetESP.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/audio/LightningTargetESP.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/audio/SoundESP.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/audio/SoundESP.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/audio/SoundESP.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/audio/SoundESP.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/BlockESP.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/BlockESP.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/BlockESP.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/BlockESP.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/DropESP.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/DropESP.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/DropESP.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/DropESP.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/HoleESP.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/HoleESP.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/HoleESP.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/HoleESP.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/InvisEsp.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/InvisEsp.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/InvisEsp.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/InvisEsp.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/render/Esp2DRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/render/Esp2DRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/render/Esp2DRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/render/Esp2DRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/render/Esp3DRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/render/Esp3DRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/render/Esp3DRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/render/Esp3DRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/render/EspOutlineRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/render/EspOutlineRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/render/EspOutlineRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/render/EspOutlineRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/render/EspSearchOverlay.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/render/EspSearchOverlay.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/render/EspSearchOverlay.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/render/EspSearchOverlay.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/shader/EspMeshBuilder.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/shader/EspMeshBuilder.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/shader/EspMeshBuilder.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/shader/EspMeshBuilder.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/shader/EspShaderColor.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/shader/EspShaderColor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/shader/EspShaderColor.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/shader/EspShaderColor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/shader/EspShaderMask.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/shader/EspShaderMask.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/shader/EspShaderMask.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/shader/EspShaderMask.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/shader/EspShaderRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/shader/EspShaderRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/shader/EspShaderRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/shader/EspShaderRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/MotionBlur.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/MotionBlur.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/MotionBlur.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/MotionBlur.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/PostFX.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/PostFX.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/PostFX.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/PostFX.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/TotemFX.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/TotemFX.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/TotemFX.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/TotemFX.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/Zoom.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/Zoom.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/Zoom.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/fx/Zoom.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/ChinaHat.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/ChinaHat.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/ChinaHat.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/ChinaHat.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/ChinaHatModel.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/ChinaHatModel.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/ChinaHatModel.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/ChinaHatModel.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/ChinaHatPalette.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/ChinaHatPalette.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/ChinaHatPalette.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/ChinaHatPalette.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/TazikHat.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/TazikHat.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/TazikHat.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/TazikHat.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/BackSword.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/BackSword.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/BackSword.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/BackSword.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/ShulkerPreview.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/ShulkerPreview.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/ShulkerPreview.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/ShulkerPreview.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/Trinket.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/Trinket.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/Trinket.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/Trinket.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/AlwaysVisibleHand.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/AlwaysVisibleHand.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/AlwaysVisibleHand.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/AlwaysVisibleHand.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/HoldMyItems.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/HoldMyItems.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/HoldMyItems.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/HoldMyItems.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/KineticLyrics.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/KineticLyrics.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/KineticLyrics.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/KineticLyrics.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/LyricParticle3D.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/LyricParticle3D.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/LyricParticle3D.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/LyricParticle3D.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/lrc/LrcLibClient.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/lrc/LrcLibClient.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/lrc/LrcLibClient.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/lrc/LrcLibClient.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/lrc/LrcParser.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/lrc/LrcParser.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/lrc/LrcParser.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/lrc/LrcParser.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/lrc/LyricLine.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/lrc/LyricLine.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/lrc/LyricLine.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/lrc/LyricLine.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/lrc/LyricLineSplitter.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/lrc/LyricLineSplitter.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/lrc/LyricLineSplitter.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/lrc/LyricLineSplitter.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/NameTagCache.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/NameTagCache.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/NameTagCache.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/NameTagCache.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/NameTagLayoutEngine.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/NameTagLayoutEngine.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/NameTagLayoutEngine.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/NameTagLayoutEngine.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/NameTagModel.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/NameTagModel.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/NameTagModel.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/NameTagModel.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/NameTags.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/NameTags.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/NameTags.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/NameTags.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/parts/NameTagEquipmentRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/parts/NameTagEquipmentRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/parts/NameTagEquipmentRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/parts/NameTagEquipmentRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/parts/NameTagFaceRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/parts/NameTagFaceRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/parts/NameTagFaceRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/parts/NameTagFaceRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/parts/NameTagOpponentRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/parts/NameTagOpponentRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/parts/NameTagOpponentRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/parts/NameTagOpponentRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/parts/NameTagPlateRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/parts/NameTagPlateRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/parts/NameTagPlateRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/parts/NameTagPlateRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/render/NameTagEffectsRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/render/NameTagEffectsRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/render/NameTagEffectsRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/render/NameTagEffectsRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/render/NameTagEnchantFormatter.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/render/NameTagEnchantFormatter.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/render/NameTagEnchantFormatter.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/render/NameTagEnchantFormatter.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/render/NameTagLabelBuilder.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/render/NameTagLabelBuilder.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/render/NameTagLabelBuilder.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/render/NameTagLabelBuilder.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/render/NameTagRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/render/NameTagRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/render/NameTagRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/nametags/render/NameTagRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/optimization/FpsOptimizer.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/optimization/FpsOptimizer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/optimization/FpsOptimizer.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/optimization/FpsOptimizer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/particles/AmbientParticles.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/particles/AmbientParticles.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/particles/AmbientParticles.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/particles/AmbientParticles.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/Animations.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/Animations.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/Animations.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/Animations.java diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/Crosshair.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/Crosshair.java new file mode 100644 index 0000000..23fdb91 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/Crosshair.java @@ -0,0 +1,143 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.visuals.player; + +import dev.loki.lovisual.config.common.CommonSettingSchemas; +import dev.loki.lovisual.config.values.color.RGBAColorValue; +import dev.loki.lovisual.config.values.mode.EnumValue; +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import dev.loki.lovisual.features.gui.hud.draggable.impl.combat.Itemizer; +import dev.loki.lovisual.features.module.core.Module; +import dev.loki.lovisual.features.module.core.ModuleCategory; +import dev.loki.lovisual.features.module.core.ModuleInfo; +import dev.loki.lovisual.features.module.phase.HudPhase; +import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors; +import dev.loki.lovisual.render.engine.core.context.ViewportContext; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import java.util.List; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; + +@ModuleInfo(id = "crosshair", displayName = "Crosshair", category = ModuleCategory.VISUALS) +public class Crosshair extends Module { + final EnumValue crosshairType = enumSetting("crosshairType", "type", CrosshairType.CLASSIC); + final EnumValue orbizMotion = visibleWhen(enumSetting("crosshairOrbizMotion", "orbiz_motion", OrbizMotionMode.DYNAMIC), this::isOrbiz); + final BooleanValue showOutline = visibleWhen(bool("crosshairOutline", "outline", true), this::isClassicCrosshair); + final BooleanValue dynamicGap = visibleWhen(bool("crosshairDynamicGap", "dynamic_gap", true), this::isClassicCrosshair); + final BooleanValue highlightTarget = bool("crosshairHighlightTarget", "highlight_target", true); + final NumberValue thickness = visibleWhen(num("crosshairThickness", "thickness", 0.5, 0.5, 1.0), this::isClassicCrosshair); + final NumberValue length = visibleWhen(num("crosshairLength", "length", 3.0, 1.0, 10.0), this::isClassicCrosshair); + final NumberValue baseGap = visibleWhen(num("crosshairBaseGap", "base_gap", 4.0, 0.0, 10.0), this::isClassicCrosshair); + final NumberValue maxGapIncrease = visibleWhen(num("crosshairMaxGapIncrease", "max_gap_increase", 7.0, 0.0, 15.0), () -> isClassicCrosshair() && dynamicGap.get()); + final NumberValue orbizRadius = visibleWhen(num("crosshairOrbizRadius", "orbiz_radius", 5.0, 2.0, 5.0), this::isOrbiz); + final NumberValue orbizThickness = visibleWhen(num("crosshairOrbizThickness", "orbiz_thickness", 1.35, 0.5, 5.0), this::isOrbiz); + final NumberValue orbizSoftness = visibleWhen(num("crosshairOrbizSoftness", "orbiz_softness", 0.35, 0.0, 2.0), this::isOrbiz); + final NumberValue orbizGlow = visibleWhen(num("crosshairOrbizGlow", "orbiz_glow", 5.5, 0.0, 20.0), this::isOrbiz); + final NumberValue orbizGlowStrength = visibleWhen(num("crosshairOrbizGlowStrength", "orbiz_glow_strength", 0.72, 0.0, 2.0), this::isOrbiz); + final NumberValue orbizCooldownMultiplier = visibleWhen(num("crosshairOrbizCooldownMultiplier", "orbiz_cooldown_multiplier", 1.0, 0.1, 1.5), this::isOrbiz); + final NumberValue orbizTrackAlpha = visibleWhen(num("crosshairOrbizTrackAlpha", "orbiz_track_alpha", 0.24, 0.0, 1.0), this::isOrbiz); + final NumberValue orbizBodyAlpha = visibleWhen(num("crosshairOrbizBodyAlpha", "orbiz_body_alpha", 0.92, 0.0, 1.0), this::isOrbiz); + final NumberValue orbizHeadBoost = visibleWhen(num("crosshairOrbizHeadBoost", "orbiz_head_boost", 0.30, 0.0, 1.0), this::isOrbiz); + final NumberValue orbizAngularCycles = visibleWhen(num("crosshairOrbizAngularCycles", "orbiz_angular_cycles", 1.0, 0.25, 4.0), this::isOrbiz); + final EnumValue orbizColorMode = visibleWhen(common(enumSetting("crosshairOrbizColorMode", "orbiz_color_mode", AnimatedRenderColors.Mode.RAINBOW, AnimatedRenderColors.Mode.STATIC, AnimatedRenderColors.Mode.RAINBOW, AnimatedRenderColors.Mode.LIGHT_RAINBOW, AnimatedRenderColors.Mode.SKY, AnimatedRenderColors.Mode.FADE, AnimatedRenderColors.Mode.DOUBLE_COLOR, AnimatedRenderColors.Mode.ANALOGOUS, AnimatedRenderColors.Mode.THEME), CommonSettingSchemas.RENDER_COLOR_MODE.commonI18nKey()), this::isOrbiz); + final NumberValue orbizColorSpeed = visibleWhen(common(num("crosshairOrbizColorSpeed", "orbiz_color_speed", 18, 2, 54), CommonSettingSchemas.RENDER_COLOR_SPEED.commonI18nKey()), this::isOrbiz); + final RGBAColorValue orbizColor = visibleWhen(common(color("crosshairOrbizColor", "orbiz_color", "#EEFFFFFF"), CommonSettingSchemas.RENDER_PRIMARY_COLOR.commonI18nKey()), this::isOrbiz); + final RGBAColorValue orbizColor2 = visibleWhen(common(color("crosshairOrbizColor2", "orbiz_color2", "#EE55FFFF"), CommonSettingSchemas.RENDER_SECONDARY_COLOR.commonI18nKey()), () -> isOrbiz() && AnimatedRenderColors.usesSecondary(orbizColorMode.get())); + final NumberValue orbizLookMultiplier = visibleWhen(num("crosshairOrbizLookMultiplier", "orbiz_look_multiplier", 0.45, 0.0, 2.5), () -> isOrbiz() && isOrbizMotionDynamic()); + final NumberValue orbizMoveMultiplier = visibleWhen(num("crosshairOrbizMoveMultiplier", "orbiz_move_multiplier", 2.0, 0.0, 10.0), () -> isOrbiz() && isOrbizMotionDynamic()); + final NumberValue orbizMotionLimit = visibleWhen(num("crosshairOrbizMotionLimit", "orbiz_motion_limit", 8.0, 0.0, 24.0), () -> isOrbiz() && isOrbizMotionDynamic()); + final NumberValue orbizMotionSmoothing = visibleWhen(num("crosshairOrbizMotionSmoothing", "orbiz_motion_smoothing", 12.0, 1.0, 30.0), () -> isOrbiz() && isOrbizMotionDynamic()); + + final CrosshairCombatHelper combat = new CrosshairCombatHelper(this); + final CrosshairColorHelper color = new CrosshairColorHelper(this); + final CrosshairOrbizHelper orbiz = new CrosshairOrbizHelper(this); + final CrosshairRenderHelper render = new CrosshairRenderHelper(this); + + @Override public HudPhase getHudPhase() { return HudPhase.LAST; } + + public boolean shouldHideVanilla() { + Minecraft mc = Minecraft.getInstance(); + return isEnabled() && mc.player != null && mc.level != null && mc.options.getCameraType().isFirstPerson(); + } + + @Override public void onEnable() { combat.resetSwapDetector(); orbiz.resetOrbizState(); color.reset(); render.reset(); } + @Override public void onDisable() { combat.resetSwapDetector(); orbiz.resetOrbizState(); Itemizer.hideElytraSwap(); } + + @Override + public void onRenderHudEngine(Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); + if (!isEnabled() || mc.player == null || mc.level == null) return; + if (!mc.options.getCameraType().isFirstPerson()) return; + float scale = ViewportContext.getScaleFactor(); + float centerX = mc.getWindow().getWidth() * 0.5f; + float centerY = mc.getWindow().getHeight() * 0.5f; + float thicknessPx = (float) (thickness.get() * scale); + float lengthPx = (float) (length.get() * scale); + float outlinePx = render.outlineExpand(scale); + float cooldown = render.computeCooldown(); + float targetGap = (float) (baseGap.get() + maxGapIncrease.get() * cooldown); + float gapPx = render.updateGap(targetGap, scale); + combat.updateSwapDetector(mc.player); + if (crosshairType.get() == CrosshairType.ORBIZ) { orbiz.renderOrbiz(renderer, centerX, centerY, scale); return; } + boolean outline = showOutline.get(); + if (crosshairType.get() == CrosshairType.DOT) { + int c = color.smoothCrosshairColor(color.resolveCrosshairTargetColor()); + render.drawSegment(renderer, centerX - thicknessPx, centerY - thicknessPx, thicknessPx * 2.0f, thicknessPx * 2.0f, c, outline, outlinePx); + return; + } + if (crosshairType.get() == CrosshairType.PINWHEEL) { + int c = color.smoothCrosshairColor(color.resolveCrosshairTargetColor()); + render.renderPinwheel(renderer, centerX, centerY, thicknessPx, lengthPx, c); + return; + } + if (combat.shouldShowRiptideBlocked(mc.player) || combat.shouldShowCooldownCross(mc.player)) { + int c = color.smoothCrosshairColor(render.riptideColor()); + render.renderRiptideBlockedCross(renderer, centerX, centerY, thicknessPx, lengthPx, outline, outlinePx, c); + return; + } + int c = color.smoothCrosshairColor(color.resolveCrosshairTargetColor()); + if (crosshairType.get() == CrosshairType.T_CLASSIC) { + render.drawSegment(renderer, centerX - lengthPx, centerY - thicknessPx * 0.5f, lengthPx * 2.0f, thicknessPx, c, outline, outlinePx); + render.drawSegment(renderer, centerX - thicknessPx * 0.5f, centerY, thicknessPx, lengthPx + gapPx, c, outline, outlinePx); + return; + } + render.drawSegment(renderer, centerX - thicknessPx * 0.5f, centerY - gapPx - lengthPx, thicknessPx, lengthPx, c, outline, outlinePx); + render.drawSegment(renderer, centerX - thicknessPx * 0.5f, centerY + gapPx, thicknessPx, lengthPx, c, outline, outlinePx); + render.drawSegment(renderer, centerX - gapPx - lengthPx, centerY - thicknessPx * 0.5f, lengthPx, thicknessPx, c, outline, outlinePx); + render.drawSegment(renderer, centerX + gapPx, centerY - thicknessPx * 0.5f, lengthPx, thicknessPx, c, outline, outlinePx); + } + + boolean isOrbiz() { return crosshairType.get() == CrosshairType.ORBIZ; } + boolean isClassicCrosshair() { return crosshairType.get() != CrosshairType.ORBIZ; } + boolean isOrbizMotionDynamic() { return orbizMotion.get() == OrbizMotionMode.DYNAMIC; } + + enum CrosshairType implements EnumValue.AliasProvider { + CLASSIC, T_CLASSIC, ORBIZ, PINWHEEL, DOT; + @Override public List aliases() { + return switch (this) { + case CLASSIC -> List.of("Classic"); + case T_CLASSIC -> List.of("T-Classic"); + case ORBIZ -> List.of("Orbiz"); + case PINWHEEL -> List.of("Pinwheel"); + case DOT -> List.of("Dot"); + }; + } + } + + enum OrbizMotionMode implements EnumValue.AliasProvider { + STATIC, DYNAMIC; + @Override public List aliases() { + return switch (this) { + case STATIC -> List.of("Static", "Off", "Disabled", "false"); + case DYNAMIC -> List.of("Dynamic", "On", "Enabled", "true"); + }; + } + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/FullBright.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/FullBright.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/FullBright.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/FullBright.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/JumpCircles.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/JumpCircles.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/JumpCircles.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/JumpCircles.java diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/crosshair/CrosshairColorHelper.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/crosshair/CrosshairColorHelper.java new file mode 100644 index 0000000..63a0538 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/crosshair/CrosshairColorHelper.java @@ -0,0 +1,86 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.visuals.player; + +import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; +import dev.loki.lovisual.features.relations.EntityFilters; +import dev.loki.lovisual.features.relations.PlayerRelations; +import dev.loki.lovisual.features.relations.category.CategoryService; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import net.minecraft.client.Minecraft; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.MobCategory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.phys.EntityHitResult; +import net.minecraft.world.phys.HitResult; + +final class CrosshairColorHelper { + + private static final int DEFAULT_COLOR = 0xFFFFFFFF; + private static final float COLOR_SMOOTH_SPEED = 14.0f; + + private final Crosshair host; + private int animatedColor = DEFAULT_COLOR; + private boolean colorInitialized; + + CrosshairColorHelper(Crosshair host) { + this.host = host; + } + + void reset() { + colorInitialized = false; + } + + int resolveCrosshairTargetColor() { + if (!host.highlightTarget.get()) return DEFAULT_COLOR; + Minecraft mc = Minecraft.getInstance(); + HitResult hit = mc.hitResult; + if (!(hit instanceof EntityHitResult ehr)) return DEFAULT_COLOR; + Entity entity = ehr.getEntity(); + if (entity == null) return DEFAULT_COLOR; + return resolveEntityColor(entity); + } + + int smoothCrosshairColor(int targetColor) { + if (!colorInitialized) { + animatedColor = targetColor; + colorInitialized = true; + return animatedColor; + } + float t = AnimationUtility.clamp01(AnimationUtility.deltaTime() * COLOR_SMOOTH_SPEED); + animatedColor = HudRenderUtil.mixColor(animatedColor, targetColor, t); + if (colorDistance(animatedColor, targetColor) <= 2) animatedColor = targetColor; + return animatedColor; + } + + private static int colorDistance(int a, int b) { + int dr = Math.abs(((a >>> 16) & 0xFF) - ((b >>> 16) & 0xFF)); + int dg = Math.abs(((a >>> 8) & 0xFF) - ((b >>> 8) & 0xFF)); + int db = Math.abs((a & 0xFF) - (b & 0xFF)); + int da = Math.abs(((a >>> 24) & 0xFF) - ((b >>> 24) & 0xFF)); + return dr + dg + db + da; + } + + private int resolveEntityColor(Entity entity) { + if (entity instanceof Player player) return CategoryService.getColor(player); + PlayerRelations rel = PlayerRelations.get(); + boolean friendly = isFriendlyEntity(entity); + return friendly ? rel.colorFriend() : rel.colorEnemy(); + } + + private boolean isFriendlyEntity(Entity entity) { + var id = BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()); + if (id != null && EntityFilters.get().isIgnoredEntity(id.toString())) return true; + return entity.getType().getCategory() != MobCategory.MONSTER; + } + + int defaultColor() { + return DEFAULT_COLOR; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/crosshair/CrosshairCombatHelper.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/crosshair/CrosshairCombatHelper.java new file mode 100644 index 0000000..5ebafa2 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/crosshair/CrosshairCombatHelper.java @@ -0,0 +1,167 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.visuals.player; + +import dev.loki.lovisual.features.gui.hud.draggable.impl.combat.Itemizer; +import dev.loki.lovisual.features.module.modules.visuals.weather.WorldTweaks; +import dev.loki.lovisual.mixininterface.entity.IPlayerAttackCooldown; +import dev.loki.lovisual.util.item.food.FoodUtil; +import net.minecraft.client.Minecraft; +import net.minecraft.core.BlockPos; +import net.minecraft.core.component.DataComponents; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.BowItem; +import net.minecraft.world.item.CrossbowItem; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.enchantment.Enchantments; +import net.minecraft.world.item.enchantment.ItemEnchantments; +import net.minecraft.world.item.equipment.Equippable; +import net.minecraft.world.level.Level; + +final class CrosshairCombatHelper { + + private final Crosshair host; + private boolean chestInit; + private boolean lastSwapCandidate; + private boolean lastChestWasElytra; + private ItemStack lastChestStack = ItemStack.EMPTY; + + CrosshairCombatHelper(Crosshair host) { + this.host = host; + } + + static boolean isRiptideBlocked(Player player, ItemStack stack) { + if (player == null || stack == null || stack.isEmpty()) return false; + if (!stack.is(Items.TRIDENT)) return false; + if (!hasRiptide(stack)) return false; + return !isTouchingWaterOrServerRain(player); + } + + private static boolean hasRiptide(ItemStack stack) { + ItemEnchantments ench = stack.getOrDefault(DataComponents.ENCHANTMENTS, ItemEnchantments.EMPTY); + return ench.keySet().stream().anyMatch(entry -> { + var keyOpt = entry.unwrapKey(); + return keyOpt.isPresent() && keyOpt.get().identifier().equals(Enchantments.RIPTIDE.identifier()); + }); + } + + private static boolean isTouchingWaterOrServerRain(Player player) { + if (player == null) return false; + if (player.isInWater()) return true; + Level world = player.level(); + BlockPos pos = player.blockPosition(); + if (WorldTweaks.hasServerRainAt(world, pos)) return true; + BlockPos top = BlockPos.containing(pos.getX(), player.getBoundingBox().maxY, pos.getZ()); + return WorldTweaks.hasServerRainAt(world, top); + } + + float getAttackCooldownProgress() { + Minecraft mc = Minecraft.getInstance(); + if (mc.player instanceof IPlayerAttackCooldown cd) { + return cd.lovisual$getAttackCooldownProgress(0.0f); + } + return mc.player.getAttackStrengthScale(0.0f); + } + + float getChargeProgress(Player player) { + if (player == null || !player.isUsingItem()) return -1.0f; + ItemStack stack = player.getUseItem(); + if (stack == null || stack.isEmpty()) return -1.0f; + int useTicks = player.getTicksUsingItem(); + Item item = stack.getItem(); + if (item instanceof BowItem) return Mth.clamp(BowItem.getPowerForTime(useTicks), 0.0f, 1.0f); + if (item instanceof CrossbowItem) { + int pullTime = CrossbowItem.getChargeDuration(stack, player); + if (pullTime <= 0) return 1.0f; + return Mth.clamp(useTicks / (float) pullTime, 0.0f, 1.0f); + } + if (stack.is(Items.TRIDENT)) return Mth.clamp(useTicks / 10.0f, 0.0f, 1.0f); + return -1.0f; + } + + boolean shouldShowRiptideBlocked(Player player) { + if (player == null) return false; + ItemStack main = player.getMainHandItem(); + if (isRiptideBlocked(player, main)) return true; + if (!isHandLikeItem(main)) return false; + return isRiptideBlocked(player, player.getOffhandItem()); + } + + boolean shouldShowCooldownCross(Player player) { + if (player == null) return false; + ItemStack main = player.getMainHandItem(); + if (isCooldownCrossItem(main) && player.getCooldowns().isOnCooldown(main)) return true; + if (shouldSuppressOffhandCooldown(main)) return false; + ItemStack off = player.getOffhandItem(); + return isCooldownCrossItem(off) && player.getCooldowns().isOnCooldown(off); + } + + private boolean isCooldownCrossItem(ItemStack stack) { + return stack != null && !stack.isEmpty(); + } + + private boolean shouldSuppressOffhandCooldown(ItemStack main) { + return !isHandLikeItem(main); + } + + private boolean isHandLikeItem(ItemStack stack) { + if (stack == null || stack.isEmpty()) return true; + Item item = stack.getItem(); + if (item instanceof BowItem || item instanceof CrossbowItem) return false; + if (stack.is(Items.TRIDENT)) return false; + if (FoodUtil.isFood(stack)) return false; + final boolean[] hasAttackMod = {false}; + stack.forEachModifier(net.minecraft.world.entity.EquipmentSlotGroup.MAINHAND, (a, m, d) -> { + if (a.equals(Attributes.ATTACK_DAMAGE) || a.equals(Attributes.ATTACK_SPEED)) hasAttackMod[0] = true; + }); + return !hasAttackMod[0]; + } + + void updateSwapDetector(Player player) { + ItemStack current = player.getItemBySlot(EquipmentSlot.CHEST); + boolean isElytra = isElytra(current); + boolean isChestplate = isChestplate(current); + boolean candidate = isElytra || isChestplate; + if (!chestInit) { + chestInit = true; + lastSwapCandidate = candidate; + lastChestWasElytra = isElytra; + lastChestStack = current.copy(); + return; + } + if (candidate && lastSwapCandidate && isElytra != lastChestWasElytra + && !ItemStack.isSameItem(current, lastChestStack)) { + Itemizer.showElytraSwap(current.copy()); + } + lastSwapCandidate = candidate; + lastChestWasElytra = isElytra; + lastChestStack = current.copy(); + } + + private boolean isElytra(ItemStack stack) { + return !stack.isEmpty() && stack.is(Items.ELYTRA); + } + + private boolean isChestplate(ItemStack stack) { + if (stack.isEmpty() || stack.is(Items.ELYTRA)) return false; + Equippable eq = stack.get(DataComponents.EQUIPPABLE); + return eq != null && eq.slot() == EquipmentSlot.CHEST; + } + + void resetSwapDetector() { + chestInit = false; + lastSwapCandidate = false; + lastChestWasElytra = false; + lastChestStack = ItemStack.EMPTY; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/crosshair/CrosshairOrbizHelper.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/crosshair/CrosshairOrbizHelper.java new file mode 100644 index 0000000..bf57001 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/crosshair/CrosshairOrbizHelper.java @@ -0,0 +1,131 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.visuals.player; + +import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors; +import dev.loki.lovisual.render.engine.animation.AnimationUtility; +import dev.loki.lovisual.render.engine.math.ColorMath; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import net.minecraft.client.Minecraft; +import net.minecraft.util.Mth; + +final class CrosshairOrbizHelper { + + private static final float ORBIZ_PROGRESS_SMOOTH_SPEED = 22.0f; + private static final float COOLDOWN_LEAD = 0.06f; + private static final int RIPTIDE_BLOCK_COLOR = 0xFFFF3B3B; + + private final Crosshair host; + private float animatedOrbizProgress = 1.0f; + private float orbizOffsetX; + private float orbizOffsetY; + private float lastOrbizYaw; + private float lastOrbizPitch; + private boolean orbizMotionInitialized; + + CrosshairOrbizHelper(Crosshair host) { + this.host = host; + } + + void resetOrbizState() { + animatedOrbizProgress = 1.0f; + orbizOffsetX = 0.0f; + orbizOffsetY = 0.0f; + lastOrbizYaw = 0.0f; + lastOrbizPitch = 0.0f; + orbizMotionInitialized = false; + } + + void renderOrbiz(Renderer2D renderer, float centerX, float centerY, float scale) { + Minecraft mc = Minecraft.getInstance(); + boolean danger = host.combat.shouldShowRiptideBlocked(mc.player) || host.combat.shouldShowCooldownCross(mc.player); + float progress = danger ? 1.0f : resolveOrbizProgress(); + float targetProgress = Mth.clamp(progress * host.orbizCooldownMultiplier.get().floatValue(), 0.0f, 1.0f); + if (targetProgress < animatedOrbizProgress) { + animatedOrbizProgress = targetProgress; + } else { + float t = AnimationUtility.clamp01(AnimationUtility.deltaTime() * ORBIZ_PROGRESS_SMOOTH_SPEED); + animatedOrbizProgress = AnimationUtility.lerp(animatedOrbizProgress, targetProgress, t); + } + updateOrbizMotion(); + AnimatedRenderColors.Mode mode = danger ? AnimatedRenderColors.Mode.STATIC : host.orbizColorMode.get(); + int primary = danger ? colorWithConfigAlpha(RIPTIDE_BLOCK_COLOR, host.orbizColor.getArgb()) : resolveOrbizPrimary(mode); + int secondary = danger ? primary : AnimatedRenderColors.angularSecondaryColor(mode, primary, host.orbizColor2.getArgb()); + primary = AnimatedRenderColors.angularPrimaryColor(mode, primary); + float radiusPx = (float) (host.orbizRadius.get() * scale); + float thicknessPx = (float) (host.orbizThickness.get() * scale); + float softnessPx = (float) (host.orbizSoftness.get() * scale); + float glowPx = (float) (host.orbizGlow.get() * scale); + float sweep = animatedOrbizProgress * 360.0f; + float trackGlow = glowPx > 0.0f ? 1.0f : 0.0f; + renderer.orbizRing( + centerX + orbizOffsetX * scale, + centerY + orbizOffsetY * scale, + radiusPx, thicknessPx, 0.0f, sweep, softnessPx, glowPx, + host.orbizGlowStrength.get().floatValue(), primary, secondary, + AnimatedRenderColors.shaderMode(mode), + AnimatedRenderColors.angularOffset01(mode, host.orbizColorSpeed.get()), + host.orbizAngularCycles.get().floatValue(), + host.orbizTrackAlpha.get().floatValue(), + host.orbizBodyAlpha.get().floatValue(), + host.orbizHeadBoost.get().floatValue(), + trackGlow, true); + } + + private float resolveOrbizProgress() { + Minecraft mc = Minecraft.getInstance(); + float charge = host.combat.getChargeProgress(mc.player); + if (charge >= 0.0f) return Mth.clamp(charge, 0.0f, 1.0f); + return Mth.clamp(host.combat.getAttackCooldownProgress() + COOLDOWN_LEAD, 0.0f, 1.0f); + } + + private void updateOrbizMotion() { + Minecraft mc = Minecraft.getInstance(); + if (mc.player == null) { + orbizOffsetX = AnimationUtility.fast(orbizOffsetX, 0.0f, 10.0f); + orbizOffsetY = AnimationUtility.fast(orbizOffsetY, 0.0f, 10.0f); + orbizMotionInitialized = false; + return; + } + float yaw = mc.player.getYRot(); + float pitch = mc.player.getXRot(); + if (!orbizMotionInitialized) { + lastOrbizYaw = yaw; + lastOrbizPitch = pitch; + orbizMotionInitialized = true; + } + float targetX = 0.0f; + float targetY = 0.0f; + if (host.isOrbizMotionDynamic()) { + float look = host.orbizLookMultiplier.get().floatValue(); + float move = host.orbizMoveMultiplier.get().floatValue(); + targetX = Mth.wrapDegrees(lastOrbizYaw - yaw) * look + mc.player.xxa * move; + targetY = (lastOrbizPitch - pitch) * look + mc.player.zza * move; + float limit = host.orbizMotionLimit.get().floatValue(); + targetX = Mth.clamp(targetX, -limit, limit); + targetY = Mth.clamp(targetY, -limit, limit); + } + float t = AnimationUtility.clamp01(AnimationUtility.deltaTime() * host.orbizMotionSmoothing.get().floatValue()); + orbizOffsetX = AnimationUtility.lerp(orbizOffsetX, targetX, t); + orbizOffsetY = AnimationUtility.lerp(orbizOffsetY, targetY, t); + lastOrbizYaw = yaw; + lastOrbizPitch = pitch; + } + + private int resolveOrbizPrimary(AnimatedRenderColors.Mode mode) { + int configured = host.orbizColor.getArgb(); + if (mode == AnimatedRenderColors.Mode.STATIC && host.highlightTarget.get()) { + return colorWithConfigAlpha(host.color.smoothCrosshairColor(host.color.resolveCrosshairTargetColor()), configured); + } + return configured; + } + + private int colorWithConfigAlpha(int rgbSource, int alphaSource) { + return ColorMath.colorWithAlpha(rgbSource, (alphaSource >>> 24) & 0xFF); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/crosshair/CrosshairRenderHelper.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/crosshair/CrosshairRenderHelper.java new file mode 100644 index 0000000..b88277b --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/player/crosshair/CrosshairRenderHelper.java @@ -0,0 +1,103 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.visuals.player; + +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import net.minecraft.util.Mth; + +final class CrosshairRenderHelper { + + private static final int OUTLINE_COLOR = 0xFF000000; + private static final int RIPTIDE_BLOCK_COLOR = 0xFFFF3B3B; + private static final float OUTLINE_EXPAND = 0.5f; + private static final float GAP_SMOOTHING = 0.15f; + private static final float RIPTIDE_CROSS_LENGTH_FACTOR = 0.65f; + private static final float PINWHEEL_SPIN_DEGREES = 130f; + + private final Crosshair host; + private float animatedGap; + + CrosshairRenderHelper(Crosshair host) { + this.host = host; + } + + float outlineExpand(float scale) { + return OUTLINE_EXPAND * scale; + } + + int outlineColor() { + return OUTLINE_COLOR; + } + + int riptideColor() { + return RIPTIDE_BLOCK_COLOR; + } + + float updateGap(float targetGap, float scale) { + animatedGap += (targetGap - animatedGap) * GAP_SMOOTHING; + return animatedGap * scale; + } + + void reset() { + animatedGap = 0f; + } + + float computeCooldown() { + net.minecraft.client.Minecraft mc = net.minecraft.client.Minecraft.getInstance(); + if (!host.dynamicGap.get()) return 0f; + float charge = host.combat.getChargeProgress(mc.player); + if (charge >= 0.0f) return 1.0f - charge; + float progress = host.combat.getAttackCooldownProgress(); + progress = Mth.clamp(progress + 0.06f, 0.0f, 1.0f); + return 1.0f - progress; + } + + void renderPinwheel(Renderer2D renderer, float centerX, float centerY, float thicknessPx, float lengthPx, int color) { + float chargeProgress = Mth.clamp(host.combat.getAttackCooldownProgress(), 0.0f, 1.0f); + float spinDeg = (1.0f - chargeProgress) * PINWHEEL_SPIN_DEGREES; + for (int i = 0; i < 4; i++) { + float bladeAngleDeg = 45f + i * 90f + spinDeg; + drawRotatedBlade(renderer, centerX, centerY, thicknessPx, lengthPx, bladeAngleDeg, color); + } + } + + private void drawRotatedBlade(Renderer2D renderer, float cx, float cy, float thicknessPx, float lengthPx, float angleDeg, int color) { + double rad = Math.toRadians(angleDeg); + double dirX = Math.cos(rad); + double dirY = Math.sin(rad); + double perpX = -dirY; + double perpY = dirX; + double innerX = cx; + double innerY = cy; + double outerX = cx + dirX * lengthPx; + double outerY = cy + dirY * lengthPx; + double halfThickness = thicknessPx * 0.5; + double[] points = new double[]{ + innerX - perpX * halfThickness, innerY - perpY * halfThickness, + outerX - perpX * halfThickness, outerY - perpY * halfThickness, + outerX + perpX * halfThickness, outerY + perpY * halfThickness, + innerX + perpX * halfThickness, innerY + perpY * halfThickness + }; + renderer.polygon(points, 4, color); + } + + void renderRiptideBlockedCross(Renderer2D renderer, float centerX, float centerY, float thicknessPx, float lengthPx, boolean outline, float outlinePx, int color) { + float crossLength = lengthPx * RIPTIDE_CROSS_LENGTH_FACTOR; + double hX = centerX - crossLength; + double hY = centerY - thicknessPx * 0.5f; + double vX = centerX - thicknessPx * 0.5f; + double vY = centerY - crossLength; + drawSegment(renderer, hX, hY, crossLength * 2.0f, thicknessPx, color, outline, outlinePx); + drawSegment(renderer, vX, vY, thicknessPx, crossLength * 2.0f, color, outline, outlinePx); + } + + void drawSegment(Renderer2D renderer, double x, double y, double w, double h, int color, boolean outline, float outlinePx) { + if (outline) renderer.quad(x - outlinePx, y - outlinePx, w + outlinePx * 2.0f, h + outlinePx * 2.0f, OUTLINE_COLOR); + renderer.quad(x, y, w, h, color); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/BlockHighlight.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/BlockHighlight.java new file mode 100644 index 0000000..9015f16 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/BlockHighlight.java @@ -0,0 +1,271 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.visuals.scene; + +import net.minecraft.client.Minecraft; +import net.minecraft.core.BlockPos; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.VoxelShape; +import dev.loki.lovisual.config.common.CommonSettingSchemas; +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.config.values.mode.EnumValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import dev.loki.lovisual.config.values.color.RGBAColorValue; +import dev.loki.lovisual.features.module.core.Module; +import dev.loki.lovisual.features.module.core.ModuleCategory; +import dev.loki.lovisual.features.module.core.ModuleInfo; +import dev.loki.lovisual.features.module.phase.WorldPhase; +import dev.loki.lovisual.render.engine.renderer.Renderer3D; + +//todo Description +@ModuleInfo( + id = "blockhighlight", + displayName = "BlockHighlight", + category = ModuleCategory.VISUALS +) +public class BlockHighlight extends Module { + + private final BlockHighlightColors colors = new BlockHighlightColors(); + private final BlockHighlightGeometry geometry = new BlockHighlightGeometry(); + private final BlockHighlightOutline outline = new BlockHighlightOutline(); + private final BlockHighlightBreaking breaking = new BlockHighlightBreaking(); + private final BooleanValue outlineEnabled = + bool( + "outline_enabled", + true + ); + private final EnumValue outlineMode = + visibleWhen(enumMode( + "outline_mode", + OutlineMode.LINES, + OutlineMode.values() + ), outlineEnabled::get); + private final RGBAColorValue outlineColor = + visibleWhen(color( + "outline_color", + "#66FFFFFF" + ), outlineEnabled::get); + private final EnumValue outlineColorMode = + visibleWhen(enumCommon( + "outline_color_mode", + "outline_color_mode", + CommonSettingSchemas.RENDER_COLOR_MODE, + ColorMode.STATIC, + ColorMode.values() + ), outlineEnabled::get); + private final RGBAColorValue outlineColor2 = + visibleWhen(color( + "outline_color2", + "#FFFFAA00" + ), this::usesSecondaryOutlineColor); + private final EnumValue outlineShaderStyle = + visibleWhen(enumMode( + "outline_shader_style", + ShaderStyle.WATER_CAUSTIC, + ShaderStyle.values() + ), this::usesShaderOutline); + private final NumberValue outlineColorSpeed = + visibleWhen(num( + "outline_color_speed", + 18, + 2, + 54 + ), outlineEnabled::get); + private final NumberValue outlineThickness = + visibleWhen(num( + "outline_thickness", + 1.0f, + 0.5f, + 4.0f + ), outlineEnabled::get); + private final BooleanValue outlineDepthTest = + visibleWhen(bool( + "outline_depth_test", + false + ), outlineEnabled::get); + private final BooleanValue breakingEnabled = + bool( + "breaking_enabled", + true + ); + private final BooleanValue breakingSelf = + visibleWhen(bool( + "breaking_self", + true + ), breakingEnabled::get); + private final BooleanValue breakingOtherPlayers = + visibleWhen(bool( + "breaking_other_players", + true + ), breakingEnabled::get); + private final EnumValue breakingAnimation = + visibleWhen(enumMode( + "breaking_animation", + BreakAnimation.GROW, + BreakAnimation.values() + ), breakingEnabled::get); + private final RGBAColorValue breakingColor = + visibleWhen(color( + "breaking_color", + "#66FFFFFF" + ), breakingEnabled::get); + private final RGBAColorValue breakingColor2 = + visibleWhen(color( + "breaking_color2", + "#22FFAA00" + ), breakingEnabled::get); + + public boolean isOutlineActive() { + return isEnabled() && outlineEnabled.get(); + } + + @Override + public WorldPhase getWorldPhase() { + return WorldPhase.BEFORE_TRANSLUCENT; + } + + @Override + public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); + if (!isEnabled() || mc.level == null || mc.player == null) return; + + if (outlineEnabled.get()) { + renderTargetOutline(renderer); + } + + if (breakingEnabled.get()) { + breaking.render(renderer, colors, geometry, breakingAnimation.get(), + breakingSelf.get(), breakingOtherPlayers.get(), + breakingColor.getArgb(), breakingColor2.getArgb()); + } + } + + private void renderTargetOutline(Renderer3D renderer) { + Minecraft mc = Minecraft.getInstance(); + HitResult hit = mc.hitResult; + if (!(hit instanceof BlockHitResult blockHit)) return; + if (hit.getType() == HitResult.Type.MISS) return; + + BlockPos pos = blockHit.getBlockPos(); + BlockState state = mc.level.getBlockState(pos); + if (state.isAir()) return; + + Entity cameraEntity = mc.getCameraEntity(); + CollisionContext context = cameraEntity != null ? CollisionContext.of(cameraEntity) : CollisionContext.empty(); + VoxelShape shape = state.getShape(mc.level, pos, context); + if (shape.isEmpty()) return; + + VoxelShape worldShape = shape.move(pos.getX(), pos.getY(), pos.getZ()); + OutlineMode mode = outlineMode.get(); + boolean depthTest = outlineDepthTest.get(); + int primary = outlineColor.getArgb(); + int secondary = outlineColor2.getArgb(); + + if (mode == OutlineMode.BOX || mode == OutlineMode.BOTH) { + outline.renderBoxes(renderer, worldShape, depthTest, colors, geometry, + outlineColorMode.get(), outlineColorSpeed.get(), primary, secondary); + } + + if (mode == OutlineMode.LINES || mode == OutlineMode.BOTH) { + outline.renderLines(renderer, worldShape, depthTest, outlineThickness.get(), colors, geometry, + outlineColorMode.get(), outlineColorSpeed.get(), primary, secondary); + } + + if (mode == OutlineMode.SHADER) { + outline.renderShader(renderer, worldShape, outlineShaderStyle.get(), colors, geometry, + primary, secondary); + } + } + + private boolean usesSecondaryOutlineColor() { + if (!outlineEnabled.get()) return false; + return colors.usesSecondary(outlineColorMode.get()); + } + + private boolean usesShaderOutline() { + return outlineEnabled.get() && outlineMode.get() == OutlineMode.SHADER; + } + + public enum ColorMode implements EnumValue.IdProvider { + STATIC("static"), + RAINBOW("rainbow"), + LIGHT_RAINBOW("light_rainbow"), + SKY("sky"), + FADE("fade"), + DOUBLE_COLOR("double_color"), + ANALOGOUS("analogous"), + THEME("theme"); + + private final String id; + + ColorMode(String id) { + this.id = id; + } + + @Override + public String id() { + return id; + } + } + + public enum OutlineMode implements EnumValue.IdProvider { + LINES("lines"), + BOX("box"), + BOTH("both"), + SHADER("shader"); + + private final String id; + + OutlineMode(String id) { + this.id = id; + } + + @Override + public String id() { + return id; + } + } + + public enum ShaderStyle implements EnumValue.IdProvider { + WATER_CAUSTIC("water_caustic"), + SCAN_LINES("scan_lines"), + HOLOGRAM("hologram"); + + private final String id; + + ShaderStyle(String id) { + this.id = id; + } + + @Override + public String id() { + return id; + } + } + + public enum BreakAnimation implements EnumValue.IdProvider { + GROW("grow"), + SHRINK("shrink"), + STATIC("static"); + + private final String id; + + BreakAnimation(String id) { + this.id = id; + } + + @Override + public String id() { + return id; + } + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/EndCrystal.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/EndCrystal.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/EndCrystal.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/EndCrystal.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/ItemPhysic.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/ItemPhysic.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/ItemPhysic.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/ItemPhysic.java diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/highlight/BlockHighlightBreaking.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/highlight/BlockHighlightBreaking.java new file mode 100644 index 0000000..9e66cef --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/highlight/BlockHighlightBreaking.java @@ -0,0 +1,128 @@ +package dev.loki.lovisual.features.module.modules.visuals.scene; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.MultiPlayerGameMode; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.BlockDestructionProgress; +import net.minecraft.util.Mth; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.shapes.CollisionContext; +import dev.loki.lovisual.mixins.accessors.misc.MultiPlayerGameModeAccessor; +import dev.loki.lovisual.render.engine.renderer.Renderer3D; + +final class BlockHighlightBreaking { + + void render(Renderer3D renderer, + BlockHighlightColors colors, + BlockHighlightGeometry geometry, + BlockHighlight.BreakAnimation animation, + boolean self, + boolean otherPlayers, + int colorArgb, + int color2Argb) { + if (self) { + renderSelf(renderer, colors, geometry, animation, colorArgb, color2Argb); + } + if (otherPlayers) { + renderOthers(renderer, colors, geometry, animation, colorArgb, color2Argb); + } + } + + private void renderSelf(Renderer3D renderer, + BlockHighlightColors colors, + BlockHighlightGeometry geometry, + BlockHighlight.BreakAnimation animation, + int colorArgb, + int color2Argb) { + Minecraft mc = Minecraft.getInstance(); + MultiPlayerGameMode interactionManager = mc.gameMode; + if (interactionManager == null || !interactionManager.isDestroying()) return; + if (!(interactionManager instanceof MultiPlayerGameModeAccessor accessor)) return; + + BlockPos pos = accessor.lovisual$getCurrentBreakingPos(); + if (pos == null) return; + + float progress = Mth.clamp(accessor.lovisual$getCurrentBreakingProgress(), 0.0f, 1.0f); + renderInfo(renderer, colors, geometry, animation, pos, progress, colorArgb, color2Argb); + } + + private void renderOthers(Renderer3D renderer, + BlockHighlightColors colors, + BlockHighlightGeometry geometry, + BlockHighlight.BreakAnimation animation, + int colorArgb, + int color2Argb) { + Minecraft mc = Minecraft.getInstance(); + if (mc.level == null) return; + + var destructionProgress = mc.level.destructionProgress(); + if (destructionProgress == null || destructionProgress.isEmpty()) return; + + destructionProgress.forEach((packedPos, infos) -> { + if (infos == null || infos.isEmpty()) return; + + BlockDestructionProgress info = infos.last(); + if (mc.player != null && info.getId() == mc.player.getId()) return; + + renderInfo(renderer, colors, geometry, animation, info.getPos(), + colors.stageToProgress(info.getProgress()), colorArgb, color2Argb); + }); + } + + private void renderInfo(Renderer3D renderer, + BlockHighlightColors colors, + BlockHighlightGeometry geometry, + BlockHighlight.BreakAnimation animation, + BlockPos pos, + float progress, + int colorArgb, + int color2Argb) { + Minecraft mc = Minecraft.getInstance(); + if (mc.level == null || pos == null) return; + + BlockState state = mc.level.getBlockState(pos); + if (state.isAir()) return; + + var shape = state.getShape(mc.level, pos, CollisionContext.empty()); + if (shape.isEmpty()) return; + + float clamped = Mth.clamp(progress, 0.0f, 1.0f); + int argb = colors.breakingArgb(colorArgb, color2Argb, clamped); + + shape.move(pos.getX(), pos.getY(), pos.getZ()).forAllBoxes((minX, minY, minZ, maxX, maxY, maxZ) -> { + AABB box = animateBox(new AABB(minX, minY, minZ, maxX, maxY, maxZ), animation, clamped); + if (isRenderable(box)) { + geometry.addFilledBox(renderer, box, argb); + } + }); + } + + private AABB animateBox(AABB box, BlockHighlight.BreakAnimation animation, float progress) { + double scale = switch (animation) { + case GROW -> progress; + case SHRINK -> 1.0 - progress; + case STATIC -> 1.0; + }; + + scale = Mth.clamp((float) scale, 0.0f, 1.0f); + if (scale >= 0.999f) { + return box; + } + + double cx = (box.minX + box.maxX) * 0.5; + double cy = (box.minY + box.maxY) * 0.5; + double cz = (box.minZ + box.maxZ) * 0.5; + double hx = (box.maxX - box.minX) * 0.5 * scale; + double hy = (box.maxY - box.minY) * 0.5 * scale; + double hz = (box.maxZ - box.minZ) * 0.5 * scale; + + return new AABB(cx - hx, cy - hy, cz - hz, cx + hx, cy + hy, cz + hz); + } + + private boolean isRenderable(AABB box) { + return box.maxX - box.minX > 1.0e-4 + && box.maxY - box.minY > 1.0e-4 + && box.maxZ - box.minZ > 1.0e-4; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/highlight/BlockHighlightColors.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/highlight/BlockHighlightColors.java new file mode 100644 index 0000000..8796c3e --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/highlight/BlockHighlightColors.java @@ -0,0 +1,87 @@ +package dev.loki.lovisual.features.module.modules.visuals.scene; + +import net.minecraft.util.Mth; +import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors; + +final class BlockHighlightColors { + + private static final double RAINBOW_SPATIAL_SCALE = 12.0; + + int outlineAt(double x, double y, double z, + BlockHighlight.ColorMode mode, + int speed, + int primaryArgb, + int secondaryArgb) { + return AnimatedRenderColors.resolve( + toAnimated(mode), + speed, + spatialIndex(x, y, z), + primaryArgb, + secondaryArgb, + true + ); + } + + int breakingArgb(int from, int to, float progress) { + return interpolateArgb(from, to, progress); + } + + boolean usesSecondary(BlockHighlight.ColorMode mode) { + return AnimatedRenderColors.usesSecondary(toAnimated(mode)); + } + + AnimatedRenderColors.Mode toAnimated(BlockHighlight.ColorMode mode) { + if (mode == null) return AnimatedRenderColors.Mode.STATIC; + return switch (mode) { + case RAINBOW -> AnimatedRenderColors.Mode.RAINBOW; + case LIGHT_RAINBOW -> AnimatedRenderColors.Mode.LIGHT_RAINBOW; + case SKY -> AnimatedRenderColors.Mode.SKY; + case FADE -> AnimatedRenderColors.Mode.FADE; + case DOUBLE_COLOR -> AnimatedRenderColors.Mode.DOUBLE_COLOR; + case ANALOGOUS -> AnimatedRenderColors.Mode.ANALOGOUS; + case THEME -> AnimatedRenderColors.Mode.THEME; + case STATIC -> AnimatedRenderColors.Mode.STATIC; + }; + } + + int spatialIndex(double x, double y, double z) { + int xs = (int) Math.round(x * (RAINBOW_SPATIAL_SCALE + 1.0)); + int ys = (int) Math.round(y * (RAINBOW_SPATIAL_SCALE - 2.0)); + int zs = (int) Math.round(z * (RAINBOW_SPATIAL_SCALE + 3.0)); + return Math.floorMod(xs + ys + zs, 3600); + } + + float stageToProgress(int stage) { + if (stage < 0) return 0.0f; + return Mth.clamp(stage / 10.0f, 0.0f, 1.0f); + } + + int interpolateArgb(int from, int to, float amount) { + amount = Mth.clamp(amount, 0.0f, 1.0f); + int a = interpolateChannel(alpha(from), alpha(to), amount); + int r = interpolateChannel(red(from), red(to), amount); + int g = interpolateChannel(green(from), green(to), amount); + int b = interpolateChannel(blue(from), blue(to), amount); + return (a << 24) | (r << 16) | (g << 8) | b; + } + + int interpolateChannel(int from, int to, float amount) { + return from + Math.round((to - from) * amount); + } + + int red(int argb) { + return (argb >>> 16) & 255; + } + + int green(int argb) { + return (argb >>> 8) & 255; + } + + int blue(int argb) { + return argb & 255; + } + + int alpha(int argb) { + return (argb >>> 24) & 255; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/highlight/BlockHighlightGeometry.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/highlight/BlockHighlightGeometry.java new file mode 100644 index 0000000..1f5677c --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/highlight/BlockHighlightGeometry.java @@ -0,0 +1,164 @@ +package dev.loki.lovisual.features.module.modules.visuals.scene; + +import net.minecraft.world.phys.AABB; +import dev.loki.lovisual.render.engine.RenderState; +import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; + +final class BlockHighlightGeometry { + + void addGradientLine(MeshBuilder mesh, + double x1, double y1, double z1, + double x2, double y2, double z2, + int argb1, int argb2) { + mesh.ensureLineCapacity(); + putColorVertex(mesh, x1, y1, z1, argb1); + int i1 = mesh.next(); + putColorVertex(mesh, x2, y2, z2, argb2); + int i2 = mesh.next(); + mesh.line(i1, i2); + } + + void addGradientQuad(MeshBuilder mesh, + double x1, double y1, double z1, int c1, + double x2, double y2, double z2, int c2, + double x3, double y3, double z3, int c3, + double x4, double y4, double z4, int c4) { + mesh.ensureQuadCapacity(); + putColorVertex(mesh, x1, y1, z1, c1); + int i1 = mesh.next(); + putColorVertex(mesh, x2, y2, z2, c2); + int i2 = mesh.next(); + putColorVertex(mesh, x3, y3, z3, c3); + int i3 = mesh.next(); + putColorVertex(mesh, x4, y4, z4, c4); + int i4 = mesh.next(); + mesh.quad(i1, i2, i3, i4); + } + + void putColorVertex(MeshBuilder mesh, double x, double y, double z, int argb) { + mesh.vec3(x, y, z).color(red(argb), green(argb), blue(argb), alpha(argb)); + } + + void addFilledBox(Renderer3D renderer, AABB box, int argb) { + int a = alpha(argb); + int r = red(argb); + int g = green(argb); + int b = blue(argb); + double minX = box.minX; + double minY = box.minY; + double minZ = box.minZ; + double maxX = box.maxX; + double maxY = box.maxY; + double maxZ = box.maxZ; + renderer.quad(minX, minY, minZ, maxX, minY, minZ, maxX, minY, maxZ, minX, minY, maxZ, r, g, b, a); + renderer.quad(minX, maxY, minZ, minX, maxY, maxZ, maxX, maxY, maxZ, maxX, maxY, minZ, r, g, b, a); + renderer.quad(minX, minY, maxZ, maxX, minY, maxZ, maxX, maxY, maxZ, minX, maxY, maxZ, r, g, b, a); + renderer.quad(minX, minY, minZ, minX, maxY, minZ, maxX, maxY, minZ, maxX, minY, minZ, r, g, b, a); + renderer.quad(maxX, minY, minZ, maxX, maxY, minZ, maxX, maxY, maxZ, maxX, minY, maxZ, r, g, b, a); + renderer.quad(minX, minY, minZ, minX, minY, maxZ, minX, maxY, maxZ, minX, maxY, minZ, r, g, b, a); + } + + void addTexturedBox(MeshBuilder mesh, + double x1, double y1, double z1, + double x2, double y2, double z2) { + addTexturedQuad(mesh, x1, y1, z1, x2, y1, z1, x2, y2, z1, x1, y2, z1); + addTexturedQuad(mesh, x2, y1, z2, x1, y1, z2, x1, y2, z2, x2, y2, z2); + addTexturedQuad(mesh, x1, y1, z2, x1, y1, z1, x1, y2, z1, x1, y2, z2); + addTexturedQuad(mesh, x2, y1, z1, x2, y1, z2, x2, y2, z2, x2, y2, z1); + addTexturedQuad(mesh, x1, y2, z1, x2, y2, z1, x2, y2, z2, x1, y2, z2); + addTexturedQuad(mesh, x1, y1, z2, x2, y1, z2, x2, y1, z1, x1, y1, z1); + } + + void addTexturedQuad(MeshBuilder mesh, + double x1, double y1, double z1, + double x2, double y2, double z2, + double x3, double y3, double z3, + double x4, double y4, double z4) { + mesh.ensureQuadCapacity(); + int i1 = mesh.vec3(x1, y1, z1).vec2(0, 0).color(255, 255, 255, 255).next(); + int i2 = mesh.vec3(x2, y2, z2).vec2(1, 0).color(255, 255, 255, 255).next(); + int i3 = mesh.vec3(x3, y3, z3).vec2(1, 1).color(255, 255, 255, 255).next(); + int i4 = mesh.vec3(x4, y4, z4).vec2(0, 1).color(255, 255, 255, 255).next(); + mesh.quad(i1, i2, i3, i4); + } + + void addGradientBox(MeshBuilder mesh, + double x1, double y1, double z1, + double x2, double y2, double z2, + int c000, int c001, int c010, int c011, + int c100, int c101, int c110, int c111) { + addGradientQuad(mesh, x1, y1, z1, c000, x2, y1, z1, c100, x2, y2, z1, c110, x1, y2, z1, c010); + addGradientQuad(mesh, x2, y1, z2, c101, x1, y1, z2, c001, x1, y2, z2, c011, x2, y2, z2, c111); + addGradientQuad(mesh, x1, y1, z2, c001, x1, y1, z1, c000, x1, y2, z1, c010, x1, y2, z2, c011); + addGradientQuad(mesh, x2, y1, z1, c100, x2, y1, z2, c101, x2, y2, z2, c111, x2, y2, z1, c110); + addGradientQuad(mesh, x1, y2, z1, c010, x2, y2, z1, c110, x2, y2, z2, c111, x1, y2, z2, c011); + addGradientQuad(mesh, x1, y1, z2, c001, x2, y1, z2, c101, x2, y1, z1, c100, x1, y1, z1, c000); + } + + void addEdgeRibbon(MeshBuilder mesh, + double x1, double y1, double z1, + double x2, double y2, double z2, + double overlap, + int argb1, int argb2, + float thickness) { + double mx = (x1 + x2) * 0.5; + double my = (y1 + y2) * 0.5; + double mz = (z1 + z2) * 0.5; + double ex = x2 - x1; + double ey = y2 - y1; + double ez = z2 - z1; + double edgeLenSq = ex * ex + ey * ey + ez * ez; + if (edgeLenSq <= 1.0e-10) return; + double toCamX = RenderState.cameraPos.x - mx; + double toCamY = RenderState.cameraPos.y - my; + double toCamZ = RenderState.cameraPos.z - mz; + double toCamLenSq = toCamX * toCamX + toCamY * toCamY + toCamZ * toCamZ; + if (toCamLenSq <= 1.0e-10) return; + double sideX = ey * toCamZ - ez * toCamY; + double sideY = ez * toCamX - ex * toCamZ; + double sideZ = ex * toCamY - ey * toCamX; + double sideLenSq = sideX * sideX + sideY * sideY + sideZ * sideZ; + if (sideLenSq <= 1.0e-10) return; + double sideInv = 1.0 / Math.sqrt(sideLenSq); + double halfWidth = computeRibbonHalfWidth(mx, my, mz, thickness); + sideX *= sideInv * halfWidth; + sideY *= sideInv * halfWidth; + sideZ *= sideInv * halfWidth; + double camInv = 1.0 / Math.sqrt(toCamLenSq); + double biasX = toCamX * camInv * overlap; + double biasY = toCamY * camInv * overlap; + double biasZ = toCamZ * camInv * overlap; + addGradientQuad( + mesh, + x1 - sideX + biasX, y1 - sideY + biasY, z1 - sideZ + biasZ, argb1, + x1 + sideX + biasX, y1 + sideY + biasY, z1 + sideZ + biasZ, argb1, + x2 + sideX + biasX, y2 + sideY + biasY, z2 + sideZ + biasZ, argb2, + x2 - sideX + biasX, y2 - sideY + biasY, z2 - sideZ + biasZ, argb2 + ); + } + + double computeRibbonHalfWidth(double x, double y, double z, float thickness) { + double dx = RenderState.cameraPos.x - x; + double dy = RenderState.cameraPos.y - y; + double dz = RenderState.cameraPos.z - z; + double dist = Math.sqrt(dx * dx + dy * dy + dz * dz); + return 0.003 + thickness * (0.0025 + dist * 0.00018); + } + + int red(int argb) { + return (argb >>> 16) & 255; + } + + int green(int argb) { + return (argb >>> 8) & 255; + } + + int blue(int argb) { + return argb & 255; + } + + int alpha(int argb) { + return (argb >>> 24) & 255; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/highlight/BlockHighlightOutline.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/highlight/BlockHighlightOutline.java new file mode 100644 index 0000000..e076ec6 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/highlight/BlockHighlightOutline.java @@ -0,0 +1,106 @@ +package dev.loki.lovisual.features.module.modules.visuals.scene; + +import net.minecraft.world.phys.shapes.VoxelShape; +import com.mojang.blaze3d.pipeline.RenderPipeline; +import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; +import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; +import dev.loki.lovisual.render.engine.uniform.impl.world.liquid.WaterHighlightUniforms; +import dev.loki.lovisual.util.time.GameClock; + +final class BlockHighlightOutline { + + private static final double GEOMETRY_EXPAND = 0.0025; + + void renderBoxes(Renderer3D renderer, + VoxelShape worldShape, + boolean depthTest, + BlockHighlightColors colors, + BlockHighlightGeometry geometry, + BlockHighlight.ColorMode colorMode, + int speed, + int primary, + int secondary) { + MeshBuilder mesh = renderer.batch( + depthTest ? LoVisualRenderPipelines.WORLD_COLORED_DEPTH : LoVisualRenderPipelines.WORLD_COLORED, + depthTest ? Renderer3D.DepthMode.PRE_DEPTH : Renderer3D.DepthMode.NONE + ); + if (mesh == null) return; + worldShape.forAllBoxes((minX, minY, minZ, maxX, maxY, maxZ) -> { + double x1 = minX - GEOMETRY_EXPAND; + double y1 = minY - GEOMETRY_EXPAND; + double z1 = minZ - GEOMETRY_EXPAND; + double x2 = maxX + GEOMETRY_EXPAND; + double y2 = maxY + GEOMETRY_EXPAND; + double z2 = maxZ + GEOMETRY_EXPAND; + int c000 = colors.outlineAt(x1, y1, z1, colorMode, speed, primary, secondary); + int c001 = colors.outlineAt(x1, y1, z2, colorMode, speed, primary, secondary); + int c010 = colors.outlineAt(x1, y2, z1, colorMode, speed, primary, secondary); + int c011 = colors.outlineAt(x1, y2, z2, colorMode, speed, primary, secondary); + int c100 = colors.outlineAt(x2, y1, z1, colorMode, speed, primary, secondary); + int c101 = colors.outlineAt(x2, y1, z2, colorMode, speed, primary, secondary); + int c110 = colors.outlineAt(x2, y2, z1, colorMode, speed, primary, secondary); + int c111 = colors.outlineAt(x2, y2, z2, colorMode, speed, primary, secondary); + geometry.addGradientBox(mesh, x1, y1, z1, x2, y2, z2, + c000, c001, c010, c011, c100, c101, c110, c111); + }); + } + + void renderLines(Renderer3D renderer, + VoxelShape worldShape, + boolean depthTest, + float thickness, + BlockHighlightColors colors, + BlockHighlightGeometry geometry, + BlockHighlight.ColorMode colorMode, + int speed, + int primary, + int secondary) { + MeshBuilder mesh = renderer.batch( + depthTest ? LoVisualRenderPipelines.WORLD_COLORED_DEPTH : LoVisualRenderPipelines.WORLD_COLORED, + depthTest ? Renderer3D.DepthMode.PRE_DEPTH : Renderer3D.DepthMode.NONE + ); + if (mesh == null) return; + double overlap = depthTest ? Math.max(0.0015, thickness * 0.003) : 0.0008; + worldShape.forAllEdges((x1, y1, z1, x2, y2, z2) -> { + int c1 = colors.outlineAt(x1, y1, z1, colorMode, speed, primary, secondary); + int c2 = colors.outlineAt(x2, y2, z2, colorMode, speed, primary, secondary); + geometry.addEdgeRibbon(mesh, x1, y1, z1, x2, y2, z2, overlap, c1, c2, thickness); + }); + } + + void renderShader(Renderer3D renderer, + VoxelShape worldShape, + BlockHighlight.ShaderStyle style, + BlockHighlightColors colors, + BlockHighlightGeometry geometry, + int primary, + int secondary) { + RenderPipeline pipeline = switch (style) { + case SCAN_LINES -> LoVisualRenderPipelines.WORLD_BLOCK_HIGHLIGHT_SCANLINES; + case HOLOGRAM -> LoVisualRenderPipelines.WORLD_BLOCK_HIGHLIGHT_HOLOGRAM; + default -> LoVisualRenderPipelines.WORLD_BLOCK_HIGHLIGHT_WATERCAUSTIC; + }; + float time = GameClock.millis() / 1000f; + WaterHighlightUniforms.update( + colors.red(primary) / 255f, colors.green(primary) / 255f, + colors.blue(primary) / 255f, colors.alpha(primary) / 255f, + colors.red(secondary) / 255f, colors.green(secondary) / 255f, + colors.blue(secondary) / 255f, colors.alpha(secondary) / 255f, + time + ); + Renderer3D.BatchBindings bindings = Renderer3D.BatchBindings.none() + .withUniform("WaterData", WaterHighlightUniforms.get()); + MeshBuilder mesh = renderer.batch(pipeline, Renderer3D.DepthMode.NONE, bindings); + if (mesh == null) return; + worldShape.forAllBoxes((minX, minY, minZ, maxX, maxY, maxZ) -> { + double x1 = minX - GEOMETRY_EXPAND; + double y1 = minY - GEOMETRY_EXPAND; + double z1 = minZ - GEOMETRY_EXPAND; + double x2 = maxX + GEOMETRY_EXPAND; + double y2 = maxY + GEOMETRY_EXPAND; + double z2 = maxZ + GEOMETRY_EXPAND; + geometry.addTexturedBox(mesh, x1, y1, z1, x2, y2, z2); + }); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/Chams.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/Chams.java new file mode 100644 index 0000000..c94b16a --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/Chams.java @@ -0,0 +1,129 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * Licensed under the GNU General Public License v3.0. + */ +package dev.loki.lovisual.features.module.modules.visuals.scene.visual; + +import com.mojang.blaze3d.textures.GpuTextureView; +import dev.loki.lovisual.config.values.color.RGBAColorValue; +import dev.loki.lovisual.config.values.color.RGBColorValue; +import dev.loki.lovisual.config.values.mode.ModeValue; +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import dev.loki.lovisual.features.module.core.Module; +import dev.loki.lovisual.features.module.core.ModuleCategory; +import dev.loki.lovisual.features.module.core.ModuleInfo; +import dev.loki.lovisual.features.module.modules.visuals.scene.visual.chams.ChamsMaskHelper; +import dev.loki.lovisual.features.module.modules.visuals.scene.visual.chams.ChamsModeHelper; +import dev.loki.lovisual.features.module.modules.visuals.scene.visual.chams.ChamsShaderHelper; +import dev.loki.lovisual.render.engine.postprocess.post.PostProcessManager; +import dev.loki.lovisual.render.engine.postprocess.post.PostProcessPass; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.client.renderer.SubmitNodeStorage; +import net.minecraft.client.renderer.feature.FeatureRenderDispatcher; +import net.minecraft.client.renderer.state.level.CameraRenderState; +import org.joml.Matrix4fc; + +@ModuleInfo(id = "chams", displayName = "Chams", category = ModuleCategory.VISUALS) +public class Chams extends Module { + public final ModeValue mode = modeSetting("handChamsMode", "mode", "Smoke", "Smoke", "Metallic", "Glass", "Noise", "Snow", "Chroma", "Solid", "Stripes", "Glow", "Balatro", "Invert"); + public final BooleanValue hands = bool("chamsHands", "hands", true); + public final RGBAColorValue fillColor = color("handChamsFillColor", "fill", "#66FFFFFF"); + public final ModeValue visualColorSource = visibleWhen(modeSetting("handChamsVisualColorSource", "visual_color_source", "Custom", "Custom", "Theme"), this::isHandEffectMode); + public final RGBColorValue metallicBase = visibleWhen(colorNoAlpha("handChamsMetallicBase", "metallic_base", "#B0B4BA"), this::isCustomMetallicVisualColor); + public final RGBColorValue metallicHighlight = visibleWhen(colorNoAlpha("handChamsMetallicHighlight", "metallic_highlight", "#FFFFFF"), this::isCustomMetallicVisualColor); + public final ModeValue effectColorSource = visibleWhen(modeSetting("handChamsEffectColorSource", "effect_color_source", "Theme", "Material", "Theme", "Custom"), this::isHandEffectMode); + public final RGBColorValue glowColor = visibleWhen(colorNoAlpha("handChamsGlowColor", "glow_color", "#87DFFF"), this::isCustomEffectColor); + public final RGBColorValue shadowColor = visibleWhen(colorNoAlpha("handChamsShadowColor", "shadow_color", "#10131A"), this::isCustomEffectColor); + public final BooleanValue glow = visibleWhen(bool("handChamsGlow", "glow", true), this::isHandEffectMode); + public final NumberValue glowAlpha = visibleWhen(num("handChamsGlowAlpha", "glow_alpha", 0.72f, 0.0f, 1.0f), this::isGlowVisible); + public final NumberValue glowStrength = visibleWhen(num("handChamsGlowStrength", "glow_strength", 1.15f, 0.0f, 6.0f), this::isGlowVisible); + public final BooleanValue shadow = visibleWhen(bool("handChamsShadow", "shadow", true), this::isHandEffectMode); + public final NumberValue shadowAlpha = visibleWhen(num("handChamsShadowAlpha", "shadow_alpha", 0.48f, 0.0f, 1.0f), this::isShadowVisible); + public final NumberValue shadowStrength = visibleWhen(num("handChamsShadowStrength", "shadow_strength", 1.0f, 0.0f, 4.0f), this::isShadowVisible); + public final NumberValue edgeWidth = visibleWhen(num("handChamsEdgeWidth", "edge_width", 10.0f, 0.0f, 36.0f), this::isHandEffectMode); + public final NumberValue quality = visibleWhen(num("handChamsQuality", "quality", 2, 1, 4), this::isHandEffectMode); + public final NumberValue smokeOctaves = visibleWhen(num("handChamsSmokeOctaves", "smoke_octaves", 4, 1, 6), this::isSmokeMode); + public final NumberValue smokeSpeed = visibleWhen(num("chamsSmokeSpeed", "smoke_speed", 0.72f, 0.0f, 3.0f), this::isSmokeMode); + public final NumberValue smokeScale = visibleWhen(num("handChamsSmokeScale", "smoke_scale", 4.4f, 1.0f, 12.0f), this::isSmokeMode); + public final NumberValue smokeContrast = visibleWhen(num("handChamsSmokeContrast", "smoke_contrast", 1.12f, 0.35f, 2.4f), this::isSmokeMode); + public final NumberValue smokeSwirl = visibleWhen(num("handChamsSmokeSwirl", "smoke_swirl", 0.85f, 0.0f, 2.5f), this::isSmokeMode); + public final NumberValue smokeDensity = visibleWhen(num("handChamsSmokeDensity", "smoke_density", 1.0f, 0.2f, 2.5f), this::isSmokeMode); + public final NumberValue noiseAlpha = visibleWhen(num("handChamsNoiseAlpha", "noise_alpha", 0.85f, 0.0f, 1.0f), this::isNoiseMode); + public final NumberValue metallicIntensity = visibleWhen(num("handChamsMetallicIntensity", "metallic_intensity", 1.35f, 0.0f, 5.0f), this::isMetallicMode); + public final NumberValue metallicSharpness = visibleWhen(num("handChamsMetallicSharpness", "metallic_sharpness", 2.2f, 0.35f, 10.0f), this::isMetallicMode); + public final NumberValue metallicEdge = visibleWhen(num("handChamsMetallicEdge", "metallic_edge", 1.25f, 0.0f, 4.0f), this::isMetallicMode); + public final NumberValue metallicSweepSpeed = visibleWhen(num("handChamsMetallicSweepSpeed", "metallic_sweep_speed", 1.15f, 0.0f, 5.0f), this::isMetallicMode); + public final NumberValue metallicSweepScale = visibleWhen(num("handChamsMetallicSweepScale", "metallic_sweep_scale", 10.0f, 1.0f, 36.0f), this::isMetallicMode); + public final NumberValue metallicBrushedLines = visibleWhen(num("handChamsMetallicBrushedLines", "metallic_brushed_lines", 0.55f, 0.0f, 1.0f), this::isMetallicMode); + public final NumberValue metallicFlakes = visibleWhen(num("handChamsMetallicFlakes", "metallic_flakes", 0.25f, 0.0f, 1.0f), this::isMetallicMode); + public final NumberValue metallicPrism = visibleWhen(num("handChamsMetallicPrism", "metallic_prism", 0.18f, 0.0f, 1.0f), this::isMetallicMode); + public final BooleanValue dualShader = bool("chamsDualShader", "dual_shader", false); + public final ModeValue shader1 = visibleWhen(modeSetting("chamsShader1", "shader1", "Glow", "Snow", "Chroma", "Solid", "Stripes", "Glow", "Balatro", "Invert"), dualShader::get); + public final ModeValue shader2 = visibleWhen(modeSetting("chamsShader2", "shader2", "Chroma", "Snow", "Chroma", "Solid", "Stripes", "Glow", "Balatro", "Invert"), dualShader::get); + public final ModeValue blendMode = visibleWhen(modeSetting("chamsBlendMode", "blend_mode", "Add", "Mix", "Add", "Multiply", "Screen", "Overlay", "Difference"), dualShader::get); + public final NumberValue blendIntensity = visibleWhen(num("chamsBlendIntensity", "blend_intensity", 1.0f, 0.0f, 1.0f), dualShader::get); + public final NumberValue shader1Strength = visibleWhen(num("chamsShader1Strength", "shader1_strength", 1.0f, 0.0f, 1.0f), dualShader::get); + public final NumberValue shader2Strength = visibleWhen(num("chamsShader2Strength", "shader2_strength", 1.0f, 0.0f, 1.0f), dualShader::get); + public final RGBColorValue snowColor = visibleWhen(colorNoAlpha("chamsSnowColor", "snow_color", "#FFFFFF"), this::isSnowMode); + public final NumberValue snowIntensity = visibleWhen(num("chamsSnowIntensity", "snow_intensity", 1.0f, 0.1f, 3.0f), this::isSnowMode); + public final NumberValue snowSpeed = visibleWhen(num("chamsSnowSpeed", "snow_speed", 1.0f, 0.1f, 2.0f), this::isSnowMode); + public final NumberValue chromaSpeed = visibleWhen(num("chamsChromaSpeed", "chroma_speed", 0.3f, 0.05f, 1.0f), this::isChromaMode); + public final NumberValue chromaSaturation = visibleWhen(num("chamsChromaSaturation", "chroma_saturation", 0.5f, 0.1f, 1.0f), this::isChromaMode); + public final NumberValue chromaBrightness = visibleWhen(num("chamsChromaBrightness", "chroma_brightness", 1.0f, 0.5f, 1.5f), this::isChromaMode); + public final RGBColorValue solidColor1 = visibleWhen(colorNoAlpha("chamsSolidColor1", "solid_color1", "#E91E63"), this::isSolidMode); + public final RGBColorValue solidColor2 = visibleWhen(colorNoAlpha("chamsSolidColor2", "solid_color2", "#9B59B6"), this::isSolidMode); + public final NumberValue solidSpeed = visibleWhen(num("chamsSolidSpeed", "solid_speed", 0.5f, 0.1f, 1.0f), this::isSolidMode); + public final RGBColorValue stripesColor1 = visibleWhen(colorNoAlpha("chamsStripesColor1", "stripes_color1", "#778889"), this::isStripesMode); + public final RGBColorValue stripesColor2 = visibleWhen(colorNoAlpha("chamsStripesColor2", "stripes_color2", "#5A6566"), this::isStripesMode); + public final NumberValue stripesWidth = visibleWhen(num("chamsStripesWidth", "stripes_width", 0.3f, 0.05f, 0.5f), this::isStripesMode); + public final NumberValue stripesSpeed = visibleWhen(num("chamsStripesSpeed", "stripes_speed", 0.3f, 0.1f, 2.0f), this::isStripesMode); + public final BooleanValue glowShaderFill = visibleWhen(bool("chamsGlowShaderFill", "glow_shader_fill", true), this::isGlowShaderMode); + public final NumberValue glowShaderRadius = visibleWhen(num("chamsGlowShaderRadius", "glow_shader_radius", 0.04f, 0.01f, 0.2f), this::isGlowShaderMode); + public final NumberValue glowShaderPower = visibleWhen(num("chamsGlowShaderPower", "glow_shader_power", 0.75f, 0.1f, 2.0f), this::isGlowShaderMode); + public final NumberValue glowShaderDispersion = visibleWhen(num("chamsGlowShaderDispersion", "glow_shader_dispersion", 5.0f, 1.0f, 10.0f), this::isGlowShaderMode); + public final RGBColorValue glowShaderColor = visibleWhen(colorNoAlpha("chamsGlowShaderColor", "glow_shader_color", "#3498DB"), this::isGlowShaderMode); + public final RGBColorValue balatroColor1 = visibleWhen(colorNoAlpha("chamsBalatroColor1", "balatro_color1", "#DE443B"), this::isBalatroMode); + public final RGBColorValue balatroColor2 = visibleWhen(colorNoAlpha("chamsBalatroColor2", "balatro_color2", "#006BB4"), this::isBalatroMode); + public final RGBColorValue balatroColor3 = visibleWhen(colorNoAlpha("chamsBalatroColor3", "balatro_color3", "#162325"), this::isBalatroMode); + public final NumberValue balatroContrast = visibleWhen(num("chamsBalatroContrast", "balatro_contrast", 0.6f, 0.1f, 1.0f), this::isBalatroMode); + public final NumberValue balatroLighting = visibleWhen(num("chamsBalatroLighting", "balatro_lighting", 0.6f, 0.0f, 1.0f), this::isBalatroMode); + public final BooleanValue hideHand = bool("chamsHideHand", "hide_hand", false); + private final ChamsMaskHelper maskHelper = new ChamsMaskHelper(this); + private final ChamsShaderHelper shaderHelper = new ChamsShaderHelper(this); + private final PostProcessPass handsPass = new HandsPass(); + { PostProcessManager.register(handsPass); } + public boolean isActive() { Minecraft mc = Minecraft.getInstance(); return isEnabled() && mc.player != null && mc.level != null; } + @Override public void onDisable() { maskHelper.onDisable(); } + public boolean renderHandMask(GameRenderer r, CameraRenderState c, float t, Matrix4fc m) { return maskHelper.renderHandMask(r, c, t, m); } + public boolean renderPreparedHandScene(FeatureRenderDispatcher d, SubmitNodeStorage s) { return maskHelper.renderPreparedHandScene(d, s); } + public boolean renderIrisHandMask(GameRenderer r, CameraRenderState c, Matrix4fc m, float t) { return maskHelper.renderIrisHandMask(r, c, m, t); } + private boolean isSmokeMode() { return ChamsModeHelper.isSmokeMode(mode.get()); } + private boolean isMetallicMode() { return ChamsModeHelper.isMetallicMode(mode.get()); } + private boolean isNoiseMode() { return ChamsModeHelper.isNoiseMode(mode.get()); } + private boolean isHandEffectMode() { return ChamsModeHelper.isHandEffectMode(mode.get()); } + private boolean isGlowVisible() { return ChamsModeHelper.isGlowVisible(mode.get(), glow.get()); } + private boolean isShadowVisible() { return ChamsModeHelper.isShadowVisible(mode.get(), shadow.get()); } + private boolean isCustomEffectColor() { return ChamsModeHelper.isCustomEffectColor(mode.get(), effectColorSource.get()); } + private boolean isCustomMetallicVisualColor() { return ChamsModeHelper.isCustomMetallicVisualColor(mode.get(), visualColorSource.get()); } + public boolean isGlassMode() { return ChamsModeHelper.isGlassMode(mode.get()); } + private boolean isSnowMode() { return ChamsModeHelper.isSnowMode(mode.get()); } + private boolean isChromaMode() { return ChamsModeHelper.isChromaMode(mode.get()); } + private boolean isSolidMode() { return ChamsModeHelper.isSolidMode(mode.get()); } + private boolean isStripesMode() { return ChamsModeHelper.isStripesMode(mode.get()); } + private boolean isGlowShaderMode() { return ChamsModeHelper.isGlowShaderMode(mode.get()); } + private boolean isBalatroMode() { return ChamsModeHelper.isBalatroMode(mode.get()); } + private boolean renderHands(GpuTextureView src, GpuTextureView dst, float tickDelta) { return shaderHelper.renderHands(maskHelper, src, dst, tickDelta); } + private final class HandsPass implements PostProcessPass { + @Override public boolean isActive() { Minecraft mc = Minecraft.getInstance(); return Chams.this.isEnabled() && mc.player != null && mc.level != null && hands.get() && mc.options.getCameraType().isFirstPerson(); } + @Override public Phase getPhase() { return Phase.POST_HAND; } + @Override public boolean render(GpuTextureView src, GpuTextureView dst, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); + if (!Chams.this.isEnabled() || mc.player == null || mc.level == null) return false; + if (!hands.get() || !mc.options.getCameraType().isFirstPerson()) return false; + return Chams.this.renderHands(src, dst, tickDelta); + } + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/ReimaginedVisual.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/ReimaginedVisual.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/ReimaginedVisual.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/ReimaginedVisual.java diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/chams/ChamsColorHelper.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/chams/ChamsColorHelper.java new file mode 100644 index 0000000..6fb0409 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/chams/ChamsColorHelper.java @@ -0,0 +1,79 @@ +package dev.loki.lovisual.features.module.modules.visuals.scene.visual.chams; + +import dev.loki.lovisual.features.theme.Theme; +import dev.loki.lovisual.features.theme.Themes; + +public final class ChamsColorHelper { + private ChamsColorHelper() {} + + public static float channel(int argb, int shift) { + return ((argb >> shift) & 0xFF) / 255.0f; + } + + public static float clamp01(float v) { + if (v < 0.0f) return 0.0f; + if (v > 1.0f) return 1.0f; + return v; + } + + public static int forceOpaque(int argb) { + return 0xFF000000 | (argb & 0x00FFFFFF); + } + + public static int mixRgb(int a, int b, float t) { + t = clamp01(t); + int ar = (a >>> 16) & 0xFF; + int ag = (a >>> 8) & 0xFF; + int ab = a & 0xFF; + int br = (b >>> 16) & 0xFF; + int bg = (b >>> 8) & 0xFF; + int bb = b & 0xFF; + int rr = Math.round(ar + (br - ar) * t); + int rg = Math.round(ag + (bg - ag) * t); + int rb = Math.round(ab + (bb - ab) * t); + return 0xFF000000 | (rr << 16) | (rg << 8) | rb; + } + + public static int rgb01(float r01, float g01) { + int r = Math.round(clamp01(r01) * 255f); + int g = Math.round(clamp01(g01) * 255f); + return 0xFF000000 | (r << 16) | (g << 8); + } + + public static int materialFillRgb(int fillArgb, String visualColorSource) { + if ("Theme".equals(visualColorSource)) { + Themes.Theme theme = Theme.theme(); + return forceOpaque(mixRgb(theme.accent(), theme.textPrimary(), 0.18f)); + } + return forceOpaque(fillArgb); + } + + public static int metallicBaseRgb(String visualColorSource, int metallicBaseArgb) { + if ("Theme".equals(visualColorSource)) { + Themes.Theme theme = Theme.theme(); + return forceOpaque(mixRgb(theme.surfaceHover(), theme.accent(), 0.42f)); + } + return forceOpaque(metallicBaseArgb); + } + + public static int metallicHighlightRgb(String visualColorSource, int metallicHighlightArgb) { + if ("Theme".equals(visualColorSource)) { + Themes.Theme theme = Theme.theme(); + return forceOpaque(mixRgb(theme.textPrimary(), theme.accentSoft(), 0.28f)); + } + return forceOpaque(metallicHighlightArgb); + } + + public static int glowRgb(String effectColorSource, int glowArgb, int materialRgb) { + if ("Custom".equals(effectColorSource)) return forceOpaque(glowArgb); + if ("Theme".equals(effectColorSource)) return forceOpaque(Theme.theme().accentSoft()); + return forceOpaque(mixRgb(materialRgb, 0xFFFFFFFF, 0.25f)); + } + + public static int shadowRgb(String effectColorSource, int shadowArgb, int materialRgb) { + if ("Custom".equals(effectColorSource)) return forceOpaque(shadowArgb); + if ("Theme".equals(effectColorSource)) + return forceOpaque(mixRgb(Theme.theme().windowBg(), Theme.theme().accent(), 0.08f)); + return forceOpaque(mixRgb(materialRgb, 0xFF05070C, 0.72f)); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/chams/ChamsMaskHelper.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/chams/ChamsMaskHelper.java new file mode 100644 index 0000000..802af90 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/chams/ChamsMaskHelper.java @@ -0,0 +1,195 @@ +package dev.loki.lovisual.features.module.modules.visuals.scene.visual.chams; + +import com.mojang.blaze3d.GpuFormat; +import com.mojang.blaze3d.pipeline.TextureTarget; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.textures.AddressMode; +import com.mojang.blaze3d.textures.FilterMode; +import com.mojang.blaze3d.textures.GpuSampler; +import com.mojang.blaze3d.textures.GpuTextureView; +import com.mojang.blaze3d.vertex.PoseStack; +import dev.loki.lovisual.features.module.lifecycle.Modules; +import dev.loki.lovisual.features.module.modules.visuals.camera.Freecam; +import dev.loki.lovisual.features.module.modules.visuals.scene.visual.Chams; +import dev.loki.lovisual.mixins.accessors.renderer.GameRendererAccessor; +import dev.loki.lovisual.mixins.iris.hand.IrisHandRendererAccessor; +import dev.loki.lovisual.render.engine.RenderState; +import dev.loki.lovisual.render.iris.runtime.IrisRuntime; +import dev.loki.lovisual.render.iris.state.IrisHandMaskState; +import net.irisshaders.iris.mixinterface.ItemInHandInterface; +import net.irisshaders.iris.pathways.HandRenderer; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.client.renderer.RenderBuffers; +import net.minecraft.client.renderer.SubmitNodeStorage; +import net.minecraft.client.renderer.feature.FeatureRenderDispatcher; +import net.minecraft.client.renderer.state.level.CameraRenderState; +import org.joml.Matrix4fc; + +public final class ChamsMaskHelper { + private final Chams chams; + private TextureTarget handMask; + private GpuSampler handMaskSampler; + private int bufferW = -1; + private int bufferH = -1; + private boolean maskReady; + private RenderBuffers irisHandRenderBuffers; + private FeatureRenderDispatcher irisHandFeatureDispatcher; + + public ChamsMaskHelper(Chams chams) { this.chams = chams; } + + public boolean isMaskReady() { return maskReady; } + public void setMaskReady(boolean v) { maskReady = v; } + public GpuTextureView getMaskView() { + return handMask == null ? null : handMask.getColorTextureView(); + } + public GpuSampler getHandMaskSampler() { + if (handMaskSampler == null) { + handMaskSampler = RenderSystem.getSamplerCache().getSampler( + AddressMode.CLAMP_TO_EDGE, AddressMode.CLAMP_TO_EDGE, + FilterMode.NEAREST, FilterMode.NEAREST, false); + } + return handMaskSampler; + } + public boolean shouldRenderHand() { + Freecam fc = Modules.get(Freecam.class); + return fc == null || !fc.isEnabled() || fc.renderHand(); + } + private void ensureBuffers() { + Minecraft mc = Minecraft.getInstance(); + int w = mc.getWindow().getWidth(); + int h = mc.getWindow().getHeight(); + if (w <= 0 || h <= 0) return; + if (handMask == null) { + handMask = new TextureTarget("lovisual-hand-mask", w, h, true, GpuFormat.RGBA8_UNORM); + bufferW = w; bufferH = h; + } else if (w != bufferW || h != bufferH) { + handMask.resize(w, h); + bufferW = w; bufferH = h; + } + } + private FeatureRenderDispatcher getStandaloneHandFeatureDispatcher() { + Minecraft mc = Minecraft.getInstance(); + if (irisHandFeatureDispatcher != null) return irisHandFeatureDispatcher; + if (mc.gameRenderer == null || mc.getModelManager() == null || mc.getAtlasManager() == null || mc.font == null) return null; + irisHandRenderBuffers = new RenderBuffers(1); + irisHandFeatureDispatcher = new FeatureRenderDispatcher( + irisHandRenderBuffers, mc.getModelManager(), mc.getAtlasManager(), mc.font, mc.gameRenderer.gameRenderState()); + return irisHandFeatureDispatcher; + } + private void endStandaloneHandFrame() { + if (irisHandRenderBuffers != null) irisHandRenderBuffers.endFrame(); + } + public void closeStandaloneHandRenderer() { + if (irisHandFeatureDispatcher != null) { irisHandFeatureDispatcher.close(); irisHandFeatureDispatcher = null; } + if (irisHandRenderBuffers != null) { irisHandRenderBuffers.close(); irisHandRenderBuffers = null; } + } + public void onDisable() { maskReady = false; closeStandaloneHandRenderer(); } + public boolean renderHandMask(GameRenderer renderer, CameraRenderState cam, float tickDelta, Matrix4fc posMatrix) { + Minecraft mc = Minecraft.getInstance(); + if (!chams.isEnabled() || mc.player == null || mc.level == null) return false; + if (mc.gameMode == null) return false; + if (!chams.hands.get() || !shouldRenderHand()) return false; + if (!mc.options.getCameraType().isFirstPerson()) return false; + maskReady = false; ensureBuffers(); + if (handMask == null) return false; + if (mc.gameRenderer.mainRenderTarget() == null) return false; + var colorTex = handMask.getColorTexture(); if (colorTex == null) return false; + var encoder = RenderSystem.getDevice().createCommandEncoder(); + encoder.clearColorTexture(colorTex, new org.joml.Vector4f(0.0f, 0.0f, 0.0f, 0.0f)); + var depthTex = handMask.getDepthTexture(); if (depthTex != null) encoder.clearDepthTexture(depthTex, 0.0); + GpuTextureView prevColor = RenderSystem.outputColorTextureOverride; + GpuTextureView prevDepth = RenderSystem.outputDepthTextureOverride; + boolean prevRendering3D = RenderState.rendering3D; + RenderSystem.outputColorTextureOverride = handMask.getColorTextureView(); + RenderSystem.outputDepthTextureOverride = handMask.getDepthTextureView(); + RenderState.rendering3D = true; + try { ((GameRendererAccessor) renderer).invokeRenderHand(cam, tickDelta, posMatrix); } + finally { + RenderState.rendering3D = prevRendering3D; + RenderSystem.outputColorTextureOverride = prevColor; + RenderSystem.outputDepthTextureOverride = prevDepth; + } + maskReady = true; return true; + } + public boolean renderPreparedHandScene(FeatureRenderDispatcher dispatcher, SubmitNodeStorage storage) { + Minecraft mc = Minecraft.getInstance(); + if (!chams.isEnabled() || mc.player == null || mc.level == null) return false; + if (dispatcher == null || storage == null) return false; + if (!chams.hands.get() || !shouldRenderHand()) return false; + if (!mc.options.getCameraType().isFirstPerson()) return false; + maskReady = false; ensureBuffers(); + if (handMask == null || handMask.getColorTexture() == null) return false; + var encoder = RenderSystem.getDevice().createCommandEncoder(); + encoder.clearColorTexture(handMask.getColorTexture(), new org.joml.Vector4f(0.0f, 0.0f, 0.0f, 0.0f)); + if (handMask.getDepthTexture() != null) encoder.clearDepthTexture(handMask.getDepthTexture(), 0.0); + try (FeatureRenderDispatcher.PreparedFrame prepared = dispatcher.prepareFrame(storage)) { + GpuTextureView prevColor = RenderSystem.outputColorTextureOverride; + GpuTextureView prevDepth = RenderSystem.outputDepthTextureOverride; + boolean prevRendering3D = RenderState.rendering3D; + RenderSystem.outputColorTextureOverride = handMask.getColorTextureView(); + RenderSystem.outputDepthTextureOverride = handMask.getDepthTextureView(); + RenderState.rendering3D = true; + try { executePreparedHandFrame(prepared); } finally { + RenderState.rendering3D = prevRendering3D; + RenderSystem.outputColorTextureOverride = prevColor; + RenderSystem.outputDepthTextureOverride = prevDepth; + } + maskReady = true; executePreparedHandFrame(prepared); + } + return true; + } + private static void executePreparedHandFrame(FeatureRenderDispatcher.PreparedFrame prepared) { + prepared.executeSolid(); prepared.executeTranslucent(); + prepared.executeTranslucentAfterTerrain(); prepared.executeAlwaysOnTop(); + } + public boolean renderIrisHandMask(GameRenderer renderer, CameraRenderState cam, Matrix4fc posMatrix, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); + if (!IrisRuntime.isShaderpackRendererActive() || !chams.isEnabled() || mc.player == null || mc.level == null) return false; + if (!chams.hands.get() || !shouldRenderHand() || !mc.options.getCameraType().isFirstPerson()) return false; + if (renderer == null || cam == null || posMatrix == null) return false; + FeatureRenderDispatcher dispatcher = getStandaloneHandFeatureDispatcher(); + if (dispatcher == null) return false; + maskReady = false; ensureBuffers(); + if (handMask == null || handMask.getColorTexture() == null) return false; + var encoder = RenderSystem.getDevice().createCommandEncoder(); + encoder.clearColorTexture(handMask.getColorTexture(), new org.joml.Vector4f(0.0f, 0.0f, 0.0f, 0.0f)); + if (handMask.getDepthTexture() != null) encoder.clearDepthTexture(handMask.getDepthTexture(), 0.0); + GpuTextureView prevColor = RenderSystem.outputColorTextureOverride; + GpuTextureView prevDepth = RenderSystem.outputDepthTextureOverride; + boolean prevRendering3D = RenderState.rendering3D; + RenderSystem.outputColorTextureOverride = handMask.getColorTextureView(); + RenderSystem.outputDepthTextureOverride = handMask.getDepthTextureView(); + RenderState.rendering3D = true; + RenderSystem.backupProjectionMatrix(); + try { + renderIrisHandMaskPhase(renderer, cam, posMatrix, dispatcher, tickDelta, true); + renderIrisHandMaskPhase(renderer, cam, posMatrix, dispatcher, tickDelta, false); + } finally { + ((IrisHandRendererAccessor) HandRenderer.INSTANCE).lovisual$setRenderingSolid(false); + RenderSystem.restoreProjectionMatrix(); + RenderState.rendering3D = prevRendering3D; + RenderSystem.outputColorTextureOverride = prevColor; + RenderSystem.outputDepthTextureOverride = prevDepth; + } + maskReady = true; return true; + } + private void renderIrisHandMaskPhase(GameRenderer renderer, CameraRenderState cam, Matrix4fc posMatrix, + FeatureRenderDispatcher dispatcher, float tickDelta, boolean solidPhase) { + IrisHandRendererAccessor irisHand = (IrisHandRendererAccessor) HandRenderer.INSTANCE; + irisHand.lovisual$setRenderingSolid(solidPhase); + PoseStack handMatrices = irisHand.lovisual$invokeSetupGlState(renderer, cam, posMatrix, tickDelta); + handMatrices.pushPose(); + var modelView = RenderSystem.getModelViewStack(); + modelView.pushMatrix(); modelView.set(handMatrices.last().pose()); + SubmitNodeStorage handSubmits = new SubmitNodeStorage(); + IrisHandMaskState.renderBothPhases(() -> + ((ItemInHandInterface) renderer.itemInHandRenderer).iris$renderHandsWithCustomRenderer( + HandRenderer.INSTANCE, tickDelta, new PoseStack(), handSubmits, + Minecraft.getInstance().player, + Minecraft.getInstance().getEntityRenderDispatcher().getPackedLightCoords(Minecraft.getInstance().player, tickDelta))); + try { dispatcher.renderAllFeatures(handSubmits); } finally { + modelView.popMatrix(); handMatrices.popPose(); endStandaloneHandFrame(); + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/chams/ChamsModeHelper.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/chams/ChamsModeHelper.java new file mode 100644 index 0000000..dd6ec8f --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/chams/ChamsModeHelper.java @@ -0,0 +1,47 @@ +package dev.loki.lovisual.features.module.modules.visuals.scene.visual.chams; + +public final class ChamsModeHelper { + private ChamsModeHelper() {} + + public static boolean isSmokeMode(String mode) { return "Smoke".equals(mode); } + public static boolean isMetallicMode(String mode) { return "Metallic".equals(mode); } + public static boolean isNoiseMode(String mode) { return "Noise".equals(mode); } + public static boolean isGlassMode(String mode) { return "Glass".equals(mode); } + public static boolean isSnowMode(String mode) { return "Snow".equals(mode); } + public static boolean isChromaMode(String mode) { return "Chroma".equals(mode); } + public static boolean isSolidMode(String mode) { return "Solid".equals(mode); } + public static boolean isStripesMode(String mode) { return "Stripes".equals(mode); } + public static boolean isGlowShaderMode(String mode) { return "Glow".equals(mode); } + public static boolean isBalatroMode(String mode) { return "Balatro".equals(mode); } + + public static boolean isHandEffectMode(String mode) { + return isSmokeMode(mode) || isMetallicMode(mode) || isNoiseMode(mode); + } + + public static boolean isGlowVisible(String mode, boolean glow) { + return isHandEffectMode(mode) && glow; + } + + public static boolean isShadowVisible(String mode, boolean shadow) { + return isHandEffectMode(mode) && shadow; + } + + public static boolean isCustomEffectColor(String mode, String effectColorSource) { + return isHandEffectMode(mode) && "Custom".equals(effectColorSource); + } + + public static boolean isCustomMetallicVisualColor(String mode, String visualColorSource) { + return isMetallicMode(mode) && "Custom".equals(visualColorSource); + } + + public static int blendModeIndex(String blendMode) { + return switch (blendMode) { + case "Add" -> 1; + case "Multiply" -> 2; + case "Screen" -> 3; + case "Overlay" -> 4; + case "Difference" -> 5; + default -> 0; + }; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/chams/ChamsShaderHelper.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/chams/ChamsShaderHelper.java new file mode 100644 index 0000000..5c5366d --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/chams/ChamsShaderHelper.java @@ -0,0 +1,193 @@ +package dev.loki.lovisual.features.module.modules.visuals.scene.visual.chams; + +import com.mojang.blaze3d.GpuFormat; +import com.mojang.blaze3d.pipeline.RenderPipeline; +import com.mojang.blaze3d.pipeline.TextureTarget; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.textures.GpuTextureView; +import dev.loki.lovisual.features.module.modules.visuals.scene.visual.Chams; +import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; +import dev.loki.lovisual.render.engine.postprocess.post.PostProcessManager; +import dev.loki.lovisual.render.engine.renderer.FullScreenRenderer; +import dev.loki.lovisual.render.engine.uniform.impl.hand.HandBlendUniforms; +import dev.loki.lovisual.render.engine.uniform.impl.hand.HandChamsUniforms; +import dev.loki.lovisual.render.engine.uniform.impl.hand.HandGlassUniforms; +import dev.loki.lovisual.render.engine.uniform.impl.hand.HandMetallicUniforms; +import dev.loki.lovisual.render.engine.uniform.impl.hand.effects.HandNoiseUniforms; +import dev.loki.lovisual.render.engine.uniform.impl.hand.effects.HandSmokeUniforms; +import net.minecraft.client.Minecraft; +import net.minecraft.util.Util; + +public final class ChamsShaderHelper { + private static final float NOISE_OCTAVES = 3.0f; + private static final float NOISE_SCALE = 3.4f; + private static final float NOISE_SPEED = 0.85f; + private static final float NOISE_CONTRAST = 1.15f; + private static final float NOISE_BREATHE = 0.55f; + private static final float NOISE_DENSITY = 1.0f; + private final Chams chams; + private TextureTarget dualA; + private TextureTarget dualB; + private int bufferW = -1; + private int bufferH = -1; + + public ChamsShaderHelper(Chams chams) { this.chams = chams; } + + private void ensureDualBuffers() { + Minecraft mc = Minecraft.getInstance(); + int w = mc.getWindow().getWidth(); + int h = mc.getWindow().getHeight(); + if (w <= 0 || h <= 0) return; + if (dualA == null) { + dualA = new TextureTarget("lovisual-hand-dual-a", w, h, true, GpuFormat.RGBA8_UNORM); + dualB = new TextureTarget("lovisual-hand-dual-b", w, h, true, GpuFormat.RGBA8_UNORM); + } else if (w != bufferW || h != bufferH) { + dualA.resize(w, h); dualB.resize(w, h); + } + bufferW = w; bufferH = h; + } + private static void clearTexture(TextureTarget target) { + if (target == null || target.getColorTexture() == null) return; + var encoder = RenderSystem.getDevice().createCommandEncoder(); + encoder.clearColorTexture(target.getColorTexture(), new org.joml.Vector4f(0.0f, 0.0f, 0.0f, 0.0f)); + } + private float getRawTime() { return (float) (Util.getMillis() / 1000.0); } + + public boolean renderHands(ChamsMaskHelper mask, GpuTextureView src, GpuTextureView dst, float tickDelta) { + if (!mask.isMaskReady()) return false; + var maskView = mask.getMaskView(); + if (maskView == null) { mask.setMaskReady(false); return false; } + float rawTime = getRawTime(); + if (chams.dualShader.get()) { renderDualShader(mask, src, dst, maskView, rawTime); mask.setMaskReady(false); return true; } + String mode = chams.mode.get(); + if ("Glass".equals(mode)) renderGlass(mask, src, dst, maskView, rawTime); + else if ("Metallic".equals(mode)) renderMetallic(mask, src, dst, maskView, rawTime); + else if ("Smoke".equals(mode)) renderSmoke(mask, src, dst, maskView, rawTime); + else if ("Noise".equals(mode)) renderNoise(mask, src, dst, maskView, rawTime); + else renderChamsEffect(mask, src, dst, maskView, mode, rawTime); + mask.setMaskReady(false); return true; + } + private void renderGlass(ChamsMaskHelper mask, GpuTextureView src, GpuTextureView dst, GpuTextureView maskView, float rawTime) { + Minecraft mc = Minecraft.getInstance(); + int fillArgb = chams.fillColor.getArgb(); + float fillR = ChamsColorHelper.channel(fillArgb, 16); + float fillG = ChamsColorHelper.channel(fillArgb, 8); + float fillB = ChamsColorHelper.channel(fillArgb, 0); + float fillA = ChamsColorHelper.channel(fillArgb, 24); + int w = mc.getWindow().getWidth(); int h = mc.getWindow().getHeight(); + float glassAlpha = chams.hideHand.get() ? 1.0f : ChamsColorHelper.clamp01(fillA); + float glassStrength = Math.max(0.0f, fillA * 4.0f); + float glassLevel = Math.min(glassStrength, 4.0f); + float edgeWidth = 12.0f + glassLevel * 2.25f; + float refractionPx = 5.5f + glassLevel * 2.35f; + float hazeStrength = 0.72f + Math.min(glassStrength, 3.0f) * 0.085f; + float frostBlurPx = 5.0f + glassLevel * 1.65f; + float bodyFrost = 0.48f + Math.min(glassStrength, 3.0f) * 0.105f; + float chromaticPx = 1.65f + glassLevel * 0.42f; + float edgeRefractionMul = 1.15f + glassLevel * 0.11f; + float clarity = 0.08f; + HandGlassUniforms.update(w, h, rawTime, edgeWidth, fillR, fillG, fillB, glassAlpha, + glassStrength, refractionPx, hazeStrength, frostBlurPx, bodyFrost, chromaticPx, edgeRefractionMul, clarity); + FullScreenRenderer.begin("LoVisual Fullscreen Pass").attachment(dst).pipeline(LoVisualRenderPipelines.HAND_GLASS) + .uniform("HandGlass", HandGlassUniforms.get()).sampler("u_Src", src, PostProcessManager.getSampler()) + .sampler("u_Mask", maskView, mask.getHandMaskSampler()).end(); + } + private void renderMetallic(ChamsMaskHelper mask, GpuTextureView src, GpuTextureView dst, GpuTextureView maskView, float rawTime) { + int fillArgb = chams.fillColor.getArgb(); + int baseRgb = ChamsColorHelper.metallicBaseRgb(chams.visualColorSource.get(), chams.metallicBase.getArgb()); + int hiRgb = ChamsColorHelper.metallicHighlightRgb(chams.visualColorSource.get(), chams.metallicHighlight.getArgb()); + int glowRgb = ChamsColorHelper.glowRgb(chams.effectColorSource.get(), chams.glowColor.getArgb(), baseRgb); + int shadowRgb = ChamsColorHelper.shadowRgb(chams.effectColorSource.get(), chams.shadowColor.getArgb(), baseRgb); + float fillA = chams.hideHand.get() ? 1.0f : ChamsColorHelper.channel(fillArgb, 24); + float glowA = chams.glow.get() ? chams.glowAlpha.get() : 0.0f; + float shadowA = chams.shadow.get() ? chams.shadowAlpha.get() : 0.0f; + HandMetallicUniforms.update(ChamsColorHelper.channel(baseRgb,16), ChamsColorHelper.channel(baseRgb,8), ChamsColorHelper.channel(baseRgb,0), fillA, + ChamsColorHelper.channel(hiRgb,16), ChamsColorHelper.channel(hiRgb,8), ChamsColorHelper.channel(hiRgb,0), 1.0f, + ChamsColorHelper.channel(glowRgb,16), ChamsColorHelper.channel(glowRgb,8), ChamsColorHelper.channel(glowRgb,0), glowA, + ChamsColorHelper.channel(shadowRgb,16), ChamsColorHelper.channel(shadowRgb,8), ChamsColorHelper.channel(shadowRgb,0), shadowA, + chams.metallicIntensity.get(), chams.metallicSharpness.get(), chams.metallicEdge.get(), rawTime, + chams.metallicSweepSpeed.get(), chams.metallicSweepScale.get(), chams.metallicBrushedLines.get(), chams.metallicFlakes.get(), + chams.glowStrength.get(), chams.shadowStrength.get(), chams.edgeWidth.get(), chams.metallicPrism.get()); + FullScreenRenderer.begin("LoVisual Fullscreen Pass").attachment(dst).pipeline(LoVisualRenderPipelines.HAND_METALLIC) + .uniform("HandMetallic", HandMetallicUniforms.get()).sampler("u_Src", src, PostProcessManager.getSampler()) + .sampler("u_Mask", maskView, mask.getHandMaskSampler()).end(); + } + private void renderSmoke(ChamsMaskHelper mask, GpuTextureView src, GpuTextureView dst, GpuTextureView maskView, float rawTime) { + Minecraft mc = Minecraft.getInstance(); + int fillArgb = chams.fillColor.getArgb(); + int fillRgb = ChamsColorHelper.materialFillRgb(fillArgb, chams.visualColorSource.get()); + int glowRgb = ChamsColorHelper.glowRgb(chams.effectColorSource.get(), chams.glowColor.getArgb(), fillRgb); + int shadowRgb = ChamsColorHelper.shadowRgb(chams.effectColorSource.get(), chams.shadowColor.getArgb(), fillRgb); + float fillA = chams.hideHand.get() ? 1.0f : ChamsColorHelper.channel(fillArgb, 24); + float glowA = chams.glow.get() ? chams.glowAlpha.get() : 0.0f; + float shadowA = chams.shadow.get() ? chams.shadowAlpha.get() : 0.0f; + int w = mc.getWindow().getWidth(); int h = mc.getWindow().getHeight(); + HandSmokeUniforms.update(ChamsColorHelper.channel(fillRgb,16), ChamsColorHelper.channel(fillRgb,8), ChamsColorHelper.channel(fillRgb,0), fillA, + ChamsColorHelper.channel(glowRgb,16), ChamsColorHelper.channel(glowRgb,8), ChamsColorHelper.channel(glowRgb,0), glowA, + ChamsColorHelper.channel(shadowRgb,16), ChamsColorHelper.channel(shadowRgb,8), ChamsColorHelper.channel(shadowRgb,0), shadowA, + chams.edgeWidth.get(), chams.quality.get(), chams.smokeOctaves.get(), rawTime * chams.smokeSpeed.get(), + w, h, chams.smokeScale.get(), chams.smokeContrast.get(), chams.smokeSwirl.get(), chams.glowStrength.get(), chams.shadowStrength.get(), chams.smokeDensity.get()); + FullScreenRenderer.begin("LoVisual Fullscreen Pass").attachment(dst).pipeline(LoVisualRenderPipelines.HAND_SMOKE) + .uniform("HandSmoke", HandSmokeUniforms.get()).sampler("u_Src", src, PostProcessManager.getSampler()) + .sampler("u_Mask", maskView, mask.getHandMaskSampler()).end(); + } + private void renderNoise(ChamsMaskHelper mask, GpuTextureView src, GpuTextureView dst, GpuTextureView maskView, float rawTime) { + Minecraft mc = Minecraft.getInstance(); + int fillRgb = ChamsColorHelper.materialFillRgb(chams.fillColor.getArgb(), chams.visualColorSource.get()); + int glowRgb = ChamsColorHelper.glowRgb(chams.effectColorSource.get(), chams.glowColor.getArgb(), fillRgb); + float fillA = chams.hideHand.get() ? 1.0f : ChamsColorHelper.clamp01(chams.noiseAlpha.get()); + float glowA = chams.glow.get() ? chams.glowAlpha.get() : 0.0f; + int w = mc.getWindow().getWidth(); int h = mc.getWindow().getHeight(); + HandNoiseUniforms.update(ChamsColorHelper.channel(fillRgb,16), ChamsColorHelper.channel(fillRgb,8), ChamsColorHelper.channel(fillRgb,0), fillA, + ChamsColorHelper.channel(glowRgb,16), ChamsColorHelper.channel(glowRgb,8), ChamsColorHelper.channel(glowRgb,0), glowA, + chams.edgeWidth.get(), chams.quality.get(), NOISE_OCTAVES, rawTime, w, h, NOISE_SCALE, NOISE_SPEED, + NOISE_CONTRAST, NOISE_BREATHE, chams.glowStrength.get(), NOISE_DENSITY); + FullScreenRenderer.begin("LoVisual Fullscreen Pass").attachment(dst).pipeline(LoVisualRenderPipelines.HAND_NOISE) + .uniform("HandNoise", HandNoiseUniforms.get()).sampler("u_Src", src, PostProcessManager.getSampler()) + .sampler("u_Mask", maskView, mask.getHandMaskSampler()).end(); + } + public void renderChamsEffect(ChamsMaskHelper mask, GpuTextureView src, GpuTextureView dst, GpuTextureView maskView, String chamsMode, float rawTime) { + Minecraft mc = Minecraft.getInstance(); + int w = mc.getWindow().getWidth(); int h = mc.getWindow().getHeight(); + float time = rawTime; + int colorA = 0xFFFFFFFF, colorB = 0xFFFFFFFF, colorC = 0xFFFFFFFF, colorD = 0xFFFFFFFF; + float chromaSpeedV = 0.3f, chromaSat = 0.5f, chromaBright = 1.0f; + float snowInt = 1.0f, snowSpd = 1.0f; float stripeW = 0.3f, stripeSpd = 0.3f; + boolean glowFill = true; float glowRadiusV = 0.04f, glowPowerV = 0.75f, glowDisp = 5.0f; + switch (chamsMode) { + case "Snow" -> { colorA = chams.snowColor.getArgb(); snowInt = chams.snowIntensity.get(); snowSpd = chams.snowSpeed.get(); } + case "Chroma" -> { chromaSpeedV = chams.chromaSpeed.get(); chromaSat = chams.chromaSaturation.get(); chromaBright = chams.chromaBrightness.get(); } + case "Solid" -> { colorA = chams.solidColor1.getArgb(); colorB = chams.solidColor2.getArgb(); time = rawTime * chams.solidSpeed.get(); } + case "Stripes" -> { colorA = chams.stripesColor1.getArgb(); colorB = chams.stripesColor2.getArgb(); stripeW = chams.stripesWidth.get(); stripeSpd = chams.stripesSpeed.get(); } + case "Glow" -> { colorC = chams.glowShaderColor.getArgb(); glowFill = chams.glowShaderFill.get(); glowRadiusV = chams.glowShaderRadius.get(); glowPowerV = chams.glowShaderPower.get(); glowDisp = chams.glowShaderDispersion.get(); } + case "Invert" -> {} + default -> { colorA = chams.balatroColor1.getArgb(); colorB = chams.balatroColor2.getArgb(); colorC = chams.balatroColor3.getArgb(); colorD = ChamsColorHelper.rgb01(chams.balatroContrast.get(), chams.balatroLighting.get()); } + } + HandChamsUniforms.update(colorA, colorB, colorC, colorD, w, h, time, + chams.hideHand.get() ? 1.0f : 0.85f, chromaSpeedV, chromaSat, chromaBright, snowInt, snowSpd, stripeW, stripeSpd, + glowFill, glowRadiusV, glowPowerV, glowDisp, chams.hideHand.get()); + RenderPipeline pipeline = switch (chamsMode) { + case "Snow" -> LoVisualRenderPipelines.HAND_CHAMS_SNOW; + case "Chroma" -> LoVisualRenderPipelines.HAND_CHAMS_CHROMA; + case "Solid" -> LoVisualRenderPipelines.HAND_CHAMS_SOLID; + case "Stripes" -> LoVisualRenderPipelines.HAND_CHAMS_STRIPES; + case "Glow" -> LoVisualRenderPipelines.HAND_CHAMS_GLOW; + case "Invert" -> LoVisualRenderPipelines.HAND_CHAMS_INVERT; + default -> LoVisualRenderPipelines.HAND_CHAMS_BALATRO; + }; + FullScreenRenderer.begin("LoVisual Fullscreen Pass").attachment(dst).pipeline(pipeline) + .uniform("HandChams", HandChamsUniforms.get()).sampler("u_Src", src, PostProcessManager.getSampler()) + .sampler("u_Mask", maskView, mask.getHandMaskSampler()).end(); + } + private void renderDualShader(ChamsMaskHelper mask, GpuTextureView src, GpuTextureView dst, GpuTextureView maskView, float rawTime) { + ensureDualBuffers(); + if (dualA == null || dualB == null || dualA.getColorTextureView() == null || dualB.getColorTextureView() == null) return; + clearTexture(dualA); clearTexture(dualB); + renderChamsEffect(mask, src, dualA.getColorTextureView(), maskView, chams.shader1.get(), rawTime); + renderChamsEffect(mask, src, dualB.getColorTextureView(), maskView, chams.shader2.get(), rawTime); + HandBlendUniforms.update(ChamsModeHelper.blendModeIndex(chams.blendMode.get()), chams.blendIntensity.get(), chams.shader1Strength.get(), chams.shader2Strength.get()); + FullScreenRenderer.begin("LoVisual Fullscreen Pass").attachment(dst).pipeline(LoVisualRenderPipelines.HAND_CHAMS_BLEND) + .uniform("HandBlend", HandBlendUniforms.get()).sampler("u_Shader1", dualA.getColorTextureView(), PostProcessManager.getSampler()) + .sampler("u_Shader2", dualB.getColorTextureView(), PostProcessManager.getSampler()).sampler("u_Mask", maskView, mask.getHandMaskSampler()).end(); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/trollface/TrollfaceMask.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/trollface/TrollfaceMask.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/trollface/TrollfaceMask.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/trollface/TrollfaceMask.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/visibility/Hitboxes.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/visibility/Hitboxes.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/visibility/Hitboxes.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/visibility/Hitboxes.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/visibility/Light.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/visibility/Light.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/visibility/Light.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/visibility/Light.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/visibility/NoRender.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/visibility/NoRender.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/visibility/NoRender.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/visibility/NoRender.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/WorldTweaks.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/WorldTweaks.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/WorldTweaks.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/WorldTweaks.java diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/precip/Rain.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/precip/Rain.java new file mode 100644 index 0000000..3ba9f71 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/precip/Rain.java @@ -0,0 +1,159 @@ +package dev.loki.lovisual.features.module.modules.visuals.weather.precip; + +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.config.values.mode.EnumValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import dev.loki.lovisual.config.values.color.RGBAColorValue; +import dev.loki.lovisual.features.module.core.Module; +import dev.loki.lovisual.features.module.core.ModuleCategory; +import dev.loki.lovisual.features.module.core.ModuleInfo; +import dev.loki.lovisual.features.module.phase.WorldPhase; +import dev.loki.lovisual.features.theme.Theme; +import dev.loki.lovisual.render.engine.RenderState; +import dev.loki.lovisual.render.engine.postprocess.post.PostProcessContext; +import dev.loki.lovisual.render.engine.postprocess.post.PostProcessManager; +import dev.loki.lovisual.render.engine.postprocess.post.PostProcessPass; +import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; +import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; +import com.mojang.blaze3d.textures.GpuTextureView; +import net.minecraft.client.Camera; +import net.minecraft.client.Minecraft; +import net.minecraft.core.BlockPos; +import net.minecraft.util.Mth; +import net.minecraft.world.phys.Vec3; +import org.joml.Matrix4f; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + +/** + * Фасад: делегирует физику/партиклы/рендер в хелперы. + */ +@ModuleInfo(id = "rain", displayName = "Rain", category = ModuleCategory.VISUALS, + description = "Реалистичный объёмный дождь с брызгами, туманом и молниями") +public final class Rain extends Module implements PostProcessPass { + static final double GRAVITY = 22.0; + static final double TERMINAL_VELOCITY = 34.0; + static final int RIPPLE_SEGMENTS = 14; + static final int MAX_DROPS = 4200; + static final int MAX_MIST = 90; + + final Matrix4f wetSurfaceProjection = new Matrix4f(); + final Matrix4f wetSurfaceInvView = new Matrix4f(); + boolean wetSurfaceDepthSupported = true; + { PostProcessManager.register(this); } + + final EnumValue preset = enumMode("preset", Preset.RAIN, Preset.values()); + final NumberValue density = num("density", 1.0f, 0.1f, 3.0f); + final NumberValue radius = num("radius", 26.0f, 8.0f, 64.0f); + final NumberValue altitude = num("altitude", 16.0f, 6.0f, 40.0f); + final NumberValue dropSize = num("drop_size", 1.0f, 0.3f, 2.5f); + final NumberValue fallSpeed = num("fall_speed", 1.0f, 0.3f, 2.5f); + final NumberValue wind = num("wind", 1.0f, 0.0f, 3.0f); + final NumberValue opacity = num("opacity", 1.0f, 0.1f, 2.0f); + final BooleanValue splashes = bool("splashes", true); + final BooleanValue droplets = visibleWhen(bool("splash_droplets", true), splashes::get); + final BooleanValue groundMist = bool("ground_mist", true); + final BooleanValue lightning = bool("lightning", false); + final BooleanValue skyCheck = bool("sky_check", true); + final BooleanValue onlyWhenRaining = bool("only_when_raining", false); + final EnumValue colorMode = enumMode("color_mode", ColorMode.REALISTIC, ColorMode.values()); + final RGBAColorValue customColor = visibleWhen(color("color", "#FFBFD8E6"), () -> colorMode.get() == ColorMode.CUSTOM); + final BooleanValue wetGround = bool("wet_ground", true); + final NumberValue wetness = visibleWhen(num("wetness", 1.25f, 0.1f, 2.0f), wetGround::get); + final NumberValue puddleCoverage = visibleWhen(num("puddle_coverage", 0.90f, 0.05f, 1.0f), wetGround::get); + final NumberValue reflectionStrength = visibleWhen(num("reflection_strength", 1.55f, 0.1f, 2.0f), wetGround::get); + final NumberValue reflectionDistance = visibleWhen(num("reflection_distance", 56.0f, 12.0f, 96.0f), wetGround::get); + final EnumValue reflectionQuality = visibleWhen(enumMode("reflection_quality", ReflectionQuality.BALANCED, ReflectionQuality.values()), wetGround::get); + final BooleanValue puddleRipples = visibleWhen(bool("puddle_ripples", true), wetGround::get); + final NumberValue rippleStrength = visibleWhen(num("ripple_strength", 0.8f, 0.0f, 1.6f), () -> wetGround.get() && puddleRipples.get()); + + final List drops = new ArrayList<>(); + final List splashList = new ArrayList<>(); + final List dropletList = new ArrayList<>(); + final List mistList = new ArrayList<>(); + final Map groundCache = new HashMap<>(); + final BlockPos.MutableBlockPos scratchPos = new BlockPos.MutableBlockPos(); + final Random random = new Random(); + long lastFrameNanos; float clock; float flash; float nextFlashIn = 9.0f; int cacheTicks; + + @Override public void onEnable() { lastFrameNanos = 0L; wetSurfaceDepthSupported = true; clearAll(); } + @Override public void onDisable() { clearAll(); } + void clearAll() { drops.clear(); splashList.clear(); dropletList.clear(); mistList.clear(); groundCache.clear(); flash = 0.0f; } + + @Override public void onTick() { + Minecraft mc = Minecraft.getInstance(); + if (mc.player == null || mc.level == null) { clearAll(); return; } + if (++cacheTicks > 30) { cacheTicks = 0; groundCache.clear(); } + } + + float random(float min, float max) { return min + random.nextFloat() * (max - min); } + + int resolveColorArgb() { + return switch (colorMode.get()) { + case CUSTOM -> customColor.getArgb(); + case THEME -> Theme.theme().accent(); + case REALISTIC -> 0xFFC8DCE8; + }; + } + + float ambientLight(Camera camera) { + Minecraft mc = Minecraft.getInstance(); + try { int light = mc.level.getBrightness(net.minecraft.world.level.LightLayer.BLOCK, camera.blockPosition()); return Mth.clamp(light / 15.0f, 0.3f, 1.0f); } + catch (Throwable ignored) { return 1.0f; } + } + + @Override public boolean isActive() { return RainWetSurface.isActiveWet(this); } + @Override public int getPriority() { return 2; } + @Override public Phase getPhase() { return Phase.PRE_HAND; } + @Override public boolean render(GpuTextureView src, GpuTextureView dst, float tickDelta) { return false; } + @Override public boolean render(PostProcessContext ctx, GpuTextureView src, GpuTextureView dst) { return RainWetSurface.renderWet(this, ctx, src, dst); } + @Override public WorldPhase getWorldPhase() { return WorldPhase.AFTER_POST_PROCESS; } + + @Override public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); + if (!isEnabled() || mc.player == null || mc.level == null || mc.gameRenderer == null) return; + if (mc.gameRenderer.mainCamera() == null) return; + if (onlyWhenRaining.get() && !mc.level.isRaining()) { clearAll(); return; } + Camera camera = mc.gameRenderer.mainCamera(); + long now = System.nanoTime(); if (lastFrameNanos == 0L) lastFrameNanos = now; + float dt = (float) ((now - lastFrameNanos) / 1_000_000_000.0); lastFrameNanos = now; + dt = Mth.clamp(dt, 0.0f, 0.05f); clock += dt; + Vec3 cam = camera.position(); + RainPhysics.updateDrops(this, cam, dt); + RainParticles.updateSplashes(this, dt); + RainParticles.updateDroplets(this, dt); + RainParticles.updateMist(this, cam, dt); + RainParticles.updateLightning(this, dt); + if (drops.isEmpty() && splashList.isEmpty() && dropletList.isEmpty() && mistList.isEmpty()) return; + int colorArgb = resolveColorArgb(); + float r = ((colorArgb >>> 16) & 0xFF) / 255f; float g = ((colorArgb >>> 8) & 0xFF) / 255f; float b = (colorArgb & 0xFF) / 255f; + float[] rgb = {r, g, b}; + float ambient = ambientLight(camera); + float brightnessMul = Mth.clamp(ambient + flash * 0.9f, 0.0f, 1.65f); + float alphaMul = opacity.get() * brightnessMul; + var camRot = RenderState.cameraRotation; float camYaw = camera.yRot(); + MeshBuilder blendMesh = renderer.batch(LoVisualRenderPipelines.WORLD_COLORED, Renderer3D.DepthMode.MAIN); + if (blendMesh != null) { + if (groundMist.get()) RainRenderer.drawMist(this, blendMesh, camRot, rgb, alphaMul); + RainRenderer.drawStreaks(this, blendMesh, camYaw, rgb, alphaMul, false); + if (splashes.get()) RainRenderer.drawRipples(this, blendMesh, rgb, alphaMul); + } + MeshBuilder additiveMesh = renderer.batch(LoVisualRenderPipelines.WORLD_COLORED_ADDITIVE_DEPTH, Renderer3D.DepthMode.PRE_DEPTH); + if (additiveMesh != null) { + RainRenderer.drawStreaks(this, additiveMesh, camYaw, rgb, alphaMul, true); + if (splashes.get()) { + RainRenderer.drawCrowns(this, additiveMesh, camYaw, rgb, alphaMul); + if (droplets.get()) RainRenderer.drawDroplets(this, additiveMesh, camYaw, rgb, alphaMul); + } + } + } + + public enum Preset implements EnumValue.IdProvider { DRIZZLE("drizzle"), RAIN("rain"), DOWNPOUR("downpour"), STORM("storm"); private final String id; Preset(String id){this.id=id;} @Override public String id(){return id;}} + public enum ColorMode implements EnumValue.IdProvider { REALISTIC("realistic"), THEME("theme"), CUSTOM("custom"); private final String id; ColorMode(String id){this.id=id;} @Override public String id(){return id;}} + public enum ReflectionQuality implements EnumValue.IdProvider { PERFORMANCE("performance"), BALANCED("balanced"), HIGH("high"); private final String id; ReflectionQuality(String id){this.id=id;} @Override public String id(){return id;}} +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/precip/WetWorld.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/precip/WetWorld.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/precip/WetWorld.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/precip/WetWorld.java diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/precip/physics/RainParticles.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/precip/physics/RainParticles.java new file mode 100644 index 0000000..c9658ad --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/precip/physics/RainParticles.java @@ -0,0 +1,113 @@ +package dev.loki.lovisual.features.module.modules.visuals.weather.precip; + +import dev.loki.lovisual.features.module.modules.visuals.weather.precip.Rain; +import net.minecraft.util.Mth; +import net.minecraft.world.phys.Vec3; + +/** + * Партиклы: Splash/Droplet/Mist + логика обновления. + */ +final class RainParticles { + private RainParticles() {} + + static void addSplash(Rain rain, double x, double y, double z, Drop d) { + if (rain.splashList.size() > 700) return; + double impactY = Math.floor(y) + 1.0005; + Splash splash = new Splash(); + splash.x = x; splash.y = impactY; splash.z = z; + splash.life = 0.0f; + splash.maxLife = rain.random(0.30f, 0.52f); + splash.scale = (float) (0.32 + 0.22 * d.mass); + splash.water = RainPhysics.isWater(rain, x, y - 0.15, z); + rain.splashList.add(splash); + if (!rain.droplets.get() || rain.dropletList.size() > 1400) return; + int shards = splash.water ? 4 : 3; + for (int i = 0; i < shards; i++) { + double a = rain.random.nextDouble() * Math.PI * 2.0; + double sp = rain.random(1.1f, 2.6f) * d.mass; + Droplet dr = new Droplet(); + dr.x = x; dr.y = impactY + 0.02; dr.z = z; + dr.vx = Math.cos(a) * sp * 0.45 + d.vx * 0.08; + dr.vz = Math.sin(a) * sp * 0.45 + d.vz * 0.08; + dr.vy = rain.random(1.9f, 4.1f); + dr.maxLife = rain.random(0.28f, 0.5f); + rain.dropletList.add(dr); + } + } + + static void updateDroplets(Rain rain, float dt) { + for (int i = rain.dropletList.size() - 1; i >= 0; i--) { + Droplet dr = rain.dropletList.get(i); + dr.life += dt; + if (dr.life >= dr.maxLife) { rain.dropletList.remove(i); continue; } + dr.vy -= Rain.GRAVITY * 0.55 * dt; + dr.x += dr.vx * dt; dr.y += dr.vy * dt; dr.z += dr.vz * dt; + } + } + + static void updateSplashes(Rain rain, float dt) { + for (int i = rain.splashList.size() - 1; i >= 0; i--) { + Splash s = rain.splashList.get(i); + s.life += dt; + if (s.life >= s.maxLife) rain.splashList.remove(i); + } + } + + static void updateMist(Rain rain, Vec3 center, float dt) { + if (!rain.groundMist.get()) { rain.mistList.clear(); return; } + int target = (int) Mth.clamp(22 * rain.density.get() * RainPhysics.presetDensity(rain), 6, Rain.MAX_MIST); + rain.mistList.removeIf(m -> { + m.life += dt; + double dx = m.x - center.x; double dz = m.z - center.z; + float r = rain.radius.get(); + m.x += m.vx * dt; m.z += m.vz * dt; + return m.life > m.maxLife || dx * dx + dz * dz > r * r; + }); + while (rain.mistList.size() < target) { + double angle = rain.random.nextDouble() * Math.PI * 2.0; + double dist = Math.sqrt(rain.random.nextDouble()) * rain.radius.get() * 0.85; + double x = center.x + Math.cos(angle) * dist; + double z = center.z + Math.sin(angle) * dist; + int ground = RainPhysics.surfaceY(rain, Mth.floor(x), Mth.floor(z)); + Mist m = new Mist(); + m.x = x; m.z = z; m.y = ground + rain.random(0.05f, 0.9f); + m.size = rain.random(2.2f, 5.6f); + m.maxLife = rain.random(2.4f, 5.5f); + m.life = rain.random(0.0f, 1.0f); + m.vx = rain.random(-0.35f, 0.35f) * rain.wind.get(); + m.vz = rain.random(-0.35f, 0.35f) * rain.wind.get(); + rain.mistList.add(m); + } + } + + static void updateLightning(Rain rain, float dt) { + if (!rain.lightning.get()) { rain.flash = 0.0f; return; } + rain.flash = Math.max(0.0f, rain.flash - dt * 3.4f); + rain.nextFlashIn -= dt; + if (rain.nextFlashIn <= 0.0f) { + rain.flash = rain.random(0.55f, 1.0f); + rain.nextFlashIn = rain.random(6.0f, 16.0f); + } + } + + static final class Drop { + double x, y, z; double vx, vy, vz; + double mass = 1.0; double ground; + float phase; float brightness = 1.0f; boolean dead; + } + + static final class Droplet { + double x, y, z; double vx, vy, vz; + float life; float maxLife = 0.4f; + } + + static final class Splash { + double x, y, z; float life; float maxLife = 0.4f; + float scale = 0.4f; boolean water; + } + + static final class Mist { + double x, y, z; double vx, vz; + float size = 3.0f; float life; float maxLife = 4.0f; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/precip/physics/RainPhysics.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/precip/physics/RainPhysics.java new file mode 100644 index 0000000..cf2faa6 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/precip/physics/RainPhysics.java @@ -0,0 +1,136 @@ +package dev.loki.lovisual.features.module.modules.visuals.weather.precip; + +import net.minecraft.core.BlockPos; +import net.minecraft.util.Mth; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.phys.Vec3; +import net.minecraft.client.Minecraft; + +/** + * Физика капель: гравитация, ветер, спавн, столкновения. + */ +final class RainPhysics { + private RainPhysics() {} + + static float presetDensity(Rain rain) { + return switch (rain.preset.get()) { + case DRIZZLE -> 0.45f; case DOWNPOUR -> 1.9f; case STORM -> 2.8f; case RAIN -> 1.0f; + }; + } + + static float presetWind(Rain rain) { + return switch (rain.preset.get()) { + case DRIZZLE -> 0.5f; case DOWNPOUR -> 1.35f; case STORM -> 2.4f; case RAIN -> 1.0f; + }; + } + + static float presetMass(Rain rain) { + return switch (rain.preset.get()) { + case DRIZZLE -> 0.55f; case DOWNPOUR -> 1.25f; case STORM -> 1.5f; case RAIN -> 1.0f; + }; + } + + static int targetDropCount(Rain rain) { + float r = rain.radius.get(); + float area = r * r * 0.0155f; + int count = (int) (area * rain.density.get() * presetDensity(rain) * 26.0f); + return Mth.clamp(count, 40, Rain.MAX_DROPS); + } + + static int surfaceY(Rain rain, int x, int z) { + Minecraft mc = Minecraft.getInstance(); + long key = (((long) x) << 32) ^ (z & 0xFFFFFFFFL); + Integer cached = rain.groundCache.get(key); + if (cached != null) return cached; + int top; + try { top = mc.level.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING, new BlockPos(x, 0, z)).getY(); } + catch (Throwable ignored) { top = mc.level.getMinY(); } + if (rain.groundCache.size() < 20000) rain.groundCache.put(key, top); + return top; + } + + static boolean isBlocking(Rain rain, double x, double y, double z) { + Minecraft mc = Minecraft.getInstance(); + if (mc.level == null) return false; + rain.scratchPos.set(Mth.floor(x), Mth.floor(y), Mth.floor(z)); + BlockState state = mc.level.getBlockState(rain.scratchPos); + if (!state.getFluidState().isEmpty()) return true; + if (state.isAir()) return false; + try { return !state.getCollisionShape(mc.level, rain.scratchPos).isEmpty(); } + catch (Throwable ignored) { return true; } + } + + static boolean isWater(Rain rain, double x, double y, double z) { + Minecraft mc = Minecraft.getInstance(); + if (mc.level == null) return false; + rain.scratchPos.set(Mth.floor(x), Mth.floor(y), Mth.floor(z)); + try { return !mc.level.getBlockState(rain.scratchPos).getFluidState().isEmpty(); } + catch (Throwable ignored) { return false; } + } + + static boolean skyVisible(Rain rain, double x, double y, double z) { + Minecraft mc = Minecraft.getInstance(); + if (!rain.skyCheck.get() || mc.level == null) return true; + rain.scratchPos.set(Mth.floor(x), Mth.floor(y), Mth.floor(z)); + try { return mc.level.canSeeSky(rain.scratchPos); } + catch (Throwable ignored) { return true; } + } + + static void spawnDrop(Rain rain, Vec3 center, boolean fromTop) { + float r = rain.radius.get(); + double angle = rain.random.nextDouble() * Math.PI * 2.0; + double dist = Math.sqrt(rain.random.nextDouble()) * r; + double x = center.x + Math.cos(angle) * dist; + double z = center.z + Math.sin(angle) * dist; + double y = fromTop ? center.y + rain.altitude.get() * rain.random(0.75f, 1.0f) + : center.y + rain.random(-4.0f, rain.altitude.get()); + if (!skyVisible(rain, x, y, z)) return; + RainParticles.Drop drop = new RainParticles.Drop(); + drop.x = x; drop.y = y; drop.z = z; + drop.mass = rain.random(0.6f, 1.4f) * presetMass(rain); + drop.vy = -rain.random(6.0f, 11.0f) * drop.mass; + drop.phase = rain.random(0.0f, 62.8f); + drop.brightness = rain.random(0.55f, 1.0f); + drop.ground = surfaceY(rain, Mth.floor(x), Mth.floor(z)); + rain.drops.add(drop); + } + + static void updateDrops(Rain rain, Vec3 center, float dt) { + int target = targetDropCount(rain); + rain.drops.removeIf(d -> { + double dx = d.x - center.x; double dz = d.z - center.z; + float r = rain.radius.get() + 6.0f; + return d.dead || dx * dx + dz * dz > r * r || d.y < center.y - rain.altitude.get() - 12.0; + }); + boolean fill = rain.drops.size() < target / 2; + int spawnBudget = fill ? Math.min(target - rain.drops.size(), 900) : Math.min(target - rain.drops.size(), 260); + for (int i = 0; i < spawnBudget; i++) spawnDrop(rain, center, !fill); + float speedMul = rain.fallSpeed.get(); + float windMul = rain.wind.get() * presetWind(rain); + for (int i = 0, size = rain.drops.size(); i < size; i++) { + RainParticles.Drop d = rain.drops.get(i); + double gust = 0.65 + 0.35 * Math.sin(rain.clock * 0.55 + d.phase * 0.15) + 0.18 * Math.sin(rain.clock * 1.7 + d.phase); + double windX = windMul * 3.1 * gust; + double windZ = windMul * 1.7 * Math.sin(rain.clock * 0.31 + d.phase * 0.05) * gust; + double drag = 1.0 - Math.min(0.92, Math.abs(d.vy) / (Rain.TERMINAL_VELOCITY * d.mass)); + d.vy -= Rain.GRAVITY * d.mass * drag * dt; + if (d.vy < -Rain.TERMINAL_VELOCITY * d.mass) d.vy = -Rain.TERMINAL_VELOCITY * d.mass; + d.vx += (windX - d.vx) * Math.min(1.0, dt * 3.2); + d.vz += (windZ - d.vz) * Math.min(1.0, dt * 3.2); + double nx = d.x + d.vx * dt * speedMul; + double ny = d.y + d.vy * dt * speedMul; + double nz = d.z + d.vz * dt * speedMul; + boolean hit = false; + if (ny <= d.ground + 2.5) { + if (isBlocking(rain, nx, ny, nz)) hit = true; + else if (ny <= d.ground - 3.0) d.dead = true; + } + if (hit) { + if (rain.splashes.get()) RainParticles.addSplash(rain, nx, ny, nz, d); + d.dead = true; continue; + } + d.x = nx; d.y = ny; d.z = nz; + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/precip/render/RainRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/precip/render/RainRenderer.java new file mode 100644 index 0000000..80fc771 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/precip/render/RainRenderer.java @@ -0,0 +1,162 @@ +package dev.loki.lovisual.features.module.modules.visuals.weather.precip; + +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; +import net.minecraft.util.Mth; +import org.joml.Quaternionf; +import org.joml.Vector3f; + +/** + * Рендер капель, брызг, ряби и тумана. + */ +final class RainRenderer { + private RainRenderer() {} + + static int packColor(float r, float g, float b, float a) { + int ai = Mth.clamp(Math.round(a * 255.0f), 0, 255); + int ri = Mth.clamp(Math.round(r * 255.0f), 0, 255); + int gi = Mth.clamp(Math.round(g * 255.0f), 0, 255); + int bi = Mth.clamp(Math.round(b * 255.0f), 0, 255); + return (ai << 24) | (ri << 16) | (gi << 8) | bi; + } + + static Vector3f rotated(Quaternionf rot, float x, float y, float z) { + return new Vector3f(x, y, z).rotate(rot); + } + + static dev.loki.lovisual.render.engine.color.RenderColor packColorObj(float[] rgb, float a) { + return new dev.loki.lovisual.render.engine.color.RenderColor(packColor(rgb[0], rgb[1], rgb[2], a)); + } + + static void drawStreaks(Rain rain, MeshBuilder mesh, float camYaw, float[] rgb, float alphaMul, boolean highlight) { + if (rain.drops.isEmpty()) return; + float sizeMul = rain.dropSize.get(); float r = rain.radius.get(); float fadeStart = r * 0.62f; + for (int i = 0, size = rain.drops.size(); i < size; i++) { + RainParticles.Drop d = rain.drops.get(i); + double speed = Math.sqrt(d.vx * d.vx + d.vy * d.vy + d.vz * d.vz); + if (speed < 0.0001) continue; + float length = (float) Mth.clamp((float) (speed * 0.055 * sizeMul), 0.16f, 2.6f); + float width = (float) (0.014 + 0.019 * d.mass) * sizeMul * (highlight ? 0.45f : 1.0f); + float yawRad = (float) Math.toRadians(camYaw); + double rightX = Math.cos(yawRad); double rightZ = Math.sin(yawRad); + double horizontal = d.vx * rightX + d.vz * rightZ; + float tilt = (float) Math.toDegrees(Math.atan2(horizontal, Math.abs(d.vy) + 0.0001)); + tilt = Mth.clamp(tilt, -55.0f, 55.0f); + Quaternionf orient = new Quaternionf().rotateY((float) Math.toRadians(-camYaw)).rotateZ((float) Math.toRadians(-tilt)); + float half = width * 0.5f; float top = length * 0.5f; float bottom = -length * 0.5f; + float headAlpha = alphaMul * d.brightness * (highlight ? 0.30f : 0.42f); + if (headAlpha <= 0.006f) continue; + headAlpha = Math.min(headAlpha, 0.95f); float tailAlpha = 0.0f; + float headWidth = half; float tailWidth = half * 0.35f; + Vector3f p1 = rotated(orient, -tailWidth, top, 0.0f); + Vector3f p2 = rotated(orient, tailWidth, top, 0.0f); + Vector3f p3 = rotated(orient, headWidth, bottom, 0.0f); + Vector3f p4 = rotated(orient, -headWidth, bottom, 0.0f); + mesh.ensureQuadCapacity(); + int i1 = mesh.vec3(d.x + p1.x, d.y + p1.y, d.z + p1.z).color(packColorObj(rgb, tailAlpha)).next(); + int i2 = mesh.vec3(d.x + p2.x, d.y + p2.y, d.z + p2.z).color(packColorObj(rgb, tailAlpha)).next(); + int i3 = mesh.vec3(d.x + p3.x, d.y + p3.y, d.z + p3.z).color(packColorObj(rgb, headAlpha)).next(); + int i4 = mesh.vec3(d.x + p4.x, d.y + p4.y, d.z + p4.z).color(packColorObj(rgb, headAlpha)).next(); + mesh.quad(i1, i2, i3, i4); + } + } + + static void drawRipples(Rain rain, MeshBuilder mesh, float[] rgb, float alphaMul) { + if (rain.splashList.isEmpty()) return; + for (int i = 0, size = rain.splashList.size(); i < size; i++) { + RainParticles.Splash s = rain.splashList.get(i); + float progress = Mth.clamp(s.life / s.maxLife, 0.0f, 1.0f); + float eased = 1.0f - (1.0f - progress) * (1.0f - progress); + float outer = s.scale * (0.18f + eased * (s.water ? 1.35f : 0.85f)); + float inner = outer * (0.55f + 0.35f * eased); + float alpha = alphaMul * (1.0f - progress) * (s.water ? 0.5f : 0.38f); + if (alpha <= 0.01f) continue; + for (int seg = 0; seg < Rain.RIPPLE_SEGMENTS; seg++) { + double a0 = (Math.PI * 2.0 / Rain.RIPPLE_SEGMENTS) * seg; + double a1 = (Math.PI * 2.0 / Rain.RIPPLE_SEGMENTS) * (seg + 1); + float ix0 = (float) (Math.cos(a0) * inner); float iz0 = (float) (Math.sin(a0) * inner); + float ix1 = (float) (Math.cos(a1) * inner); float iz1 = (float) (Math.sin(a1) * inner); + float ox0 = (float) (Math.cos(a0) * outer); float oz0 = (float) (Math.sin(a0) * outer); + float ox1 = (float) (Math.cos(a1) * outer); float oz1 = (float) (Math.sin(a1) * outer); + mesh.ensureQuadCapacity(); + int v1 = mesh.vec3(s.x + ix0, s.y, s.z + iz0).color(packColorObj(rgb, alpha)).next(); + int v2 = mesh.vec3(s.x + ix1, s.y, s.z + iz1).color(packColorObj(rgb, alpha)).next(); + int v3 = mesh.vec3(s.x + ox1, s.y, s.z + oz1).color(packColorObj(rgb, 0.0f)).next(); + int v4 = mesh.vec3(s.x + ox0, s.y, s.z + oz0).color(packColorObj(rgb, 0.0f)).next(); + mesh.quad(v1, v2, v3, v4); + } + } + } + + static void drawCrowns(Rain rain, MeshBuilder mesh, float camYaw, float[] rgb, float alphaMul) { + if (rain.splashList.isEmpty()) return; + Quaternionf orient = new Quaternionf().rotateY((float) Math.toRadians(-camYaw)); + for (int i = 0, size = rain.splashList.size(); i < size; i++) { + RainParticles.Splash s = rain.splashList.get(i); + float progress = Mth.clamp(s.life / s.maxLife, 0.0f, 1.0f); + if (progress > 0.55f) continue; + float local = progress / 0.55f; + float height = s.scale * (0.55f * (float) Math.sin(local * Math.PI)); + float alpha = alphaMul * (1.0f - local) * 0.32f; + if (alpha <= 0.01f || height <= 0.005f) continue; + float base = s.scale * 0.16f; + Vector3f p1 = rotated(orient, -base * 0.25f, height, 0.0f); + Vector3f p2 = rotated(orient, base * 0.25f, height, 0.0f); + Vector3f p3 = rotated(orient, base, 0.0f, 0.0f); + Vector3f p4 = rotated(orient, -base, 0.0f, 0.0f); + mesh.ensureQuadCapacity(); + int i1 = mesh.vec3(s.x + p1.x, s.y + p1.y, s.z + p1.z).color(packColorObj(rgb, 0.0f)).next(); + int i2 = mesh.vec3(s.x + p2.x, s.y + p2.y, s.z + p2.z).color(packColorObj(rgb, 0.0f)).next(); + int i3 = mesh.vec3(s.x + p3.x, s.y + p3.y, s.z + p3.z).color(packColorObj(rgb, alpha)).next(); + int i4 = mesh.vec3(s.x + p4.x, s.y + p4.y, s.z + p4.z).color(packColorObj(rgb, alpha)).next(); + mesh.quad(i1, i2, i3, i4); + } + } + + static void drawDroplets(Rain rain, MeshBuilder mesh, float camYaw, float[] rgb, float alphaMul) { + if (rain.dropletList.isEmpty()) return; + float sizeMul = rain.dropSize.get(); float yawRad = (float) Math.toRadians(camYaw); + for (int i = 0, size = rain.dropletList.size(); i < size; i++) { + RainParticles.Droplet d = rain.dropletList.get(i); + float progress = Mth.clamp(d.life / d.maxLife, 0.0f, 1.0f); + float alpha = alphaMul * (1.0f - progress) * 0.5f; + if (alpha <= 0.01f) continue; + double speed = Math.sqrt(d.vx * d.vx + d.vy * d.vy + d.vz * d.vz); + float length = (float) Mth.clamp((float) (speed * 0.03 * sizeMul), 0.04f, 0.4f); + float width = 0.016f * sizeMul; + double horizontal = d.vx * Math.cos(yawRad) + d.vz * Math.sin(yawRad); + float tilt = (float) Math.toDegrees(Math.atan2(horizontal, -d.vy + 0.0001)); + Quaternionf orient = new Quaternionf().rotateY((float) Math.toRadians(-camYaw)).rotateZ((float) Math.toRadians(-tilt)); + Vector3f p1 = rotated(orient, -width * 0.4f, length * 0.5f, 0.0f); + Vector3f p2 = rotated(orient, width * 0.4f, length * 0.5f, 0.0f); + Vector3f p3 = rotated(orient, width, -length * 0.5f, 0.0f); + Vector3f p4 = rotated(orient, -width, -length * 0.5f, 0.0f); + mesh.ensureQuadCapacity(); + int i1 = mesh.vec3(d.x + p1.x, d.y + p1.y, d.z + p1.z).color(packColorObj(rgb, 0.0f)).next(); + int i2 = mesh.vec3(d.x + p2.x, d.y + p2.y, d.z + p2.z).color(packColorObj(rgb, 0.0f)).next(); + int i3 = mesh.vec3(d.x + p3.x, d.y + p3.y, d.z + p3.z).color(packColorObj(rgb, alpha)).next(); + int i4 = mesh.vec3(d.x + p4.x, d.y + p4.y, d.z + p4.z).color(packColorObj(rgb, alpha)).next(); + mesh.quad(i1, i2, i3, i4); + } + } + + static void drawMist(Rain rain, MeshBuilder mesh, Quaternionf camRot, float[] rgb, float alphaMul) { + if (rain.mistList.isEmpty()) return; + for (int i = 0, size = rain.mistList.size(); i < size; i++) { + RainParticles.Mist mist = rain.mistList.get(i); + float progress = Mth.clamp(mist.life / mist.maxLife, 0.0f, 1.0f); + float fade = (float) Math.sin(progress * Math.PI); + float alpha = alphaMul * fade * 0.05f; + if (alpha <= 0.004f) continue; + float half = mist.size * 0.5f; float top = mist.size * 0.42f; + Vector3f right = new Vector3f(1.0f, 0.0f, 0.0f).rotate(camRot); + Vector3f up = new Vector3f(0.0f, 1.0f, 0.0f).rotate(camRot); + double bx = mist.x, by = mist.y, bz = mist.z; + mesh.ensureQuadCapacity(); + int i1 = mesh.vec3(bx - right.x * half, by - right.y * half + up.y * top, bz - right.z * half).color(packColorObj(rgb, 0.0f)).next(); + int i2 = mesh.vec3(bx + right.x * half, by + right.y * half + up.y * top, bz + right.z * half).color(packColorObj(rgb, 0.0f)).next(); + int i3 = mesh.vec3(bx + right.x * half, by + right.y * half, bz + right.z * half).color(packColorObj(rgb, alpha)).next(); + int i4 = mesh.vec3(bx - right.x * half, by - right.y * half, bz - right.z * half).color(packColorObj(rgb, alpha)).next(); + mesh.quad(i1, i2, i3, i4); + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/precip/render/RainWetSurface.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/precip/render/RainWetSurface.java new file mode 100644 index 0000000..79ea0a3 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/precip/render/RainWetSurface.java @@ -0,0 +1,77 @@ +package dev.loki.lovisual.features.module.modules.visuals.weather.precip; + +import com.mojang.blaze3d.textures.GpuTextureView; +import dev.loki.lovisual.render.engine.RenderState; +import dev.loki.lovisual.render.engine.core.system.LoVisualWorldMatrices; +import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; +import dev.loki.lovisual.render.engine.postprocess.post.PostProcessContext; +import dev.loki.lovisual.render.engine.postprocess.post.PostProcessManager; +import dev.loki.lovisual.render.engine.renderer.FullScreenRenderer; +import dev.loki.lovisual.render.engine.uniform.impl.world.liquid.WetSurfaceUniforms; +import net.minecraft.client.Minecraft; +import net.minecraft.util.Mth; +import org.joml.Matrix4f; + +/** + * Пресеты + wet-ground SSR pass. + */ +final class RainWetSurface { + private RainWetSurface() {} + + static int reflectionSteps(Rain rain) { + return switch (rain.reflectionQuality.get()) { + case HIGH -> 16; case BALANCED -> 10; case PERFORMANCE -> 6; + }; + } + + static int binarySteps(Rain rain) { + return switch (rain.reflectionQuality.get()) { + case HIGH -> 6; case BALANCED -> 4; case PERFORMANCE -> 3; + }; + } + + static float rainAmount(Rain rain) { + return Mth.clamp(0.55f + rain.density.get() * RainPhysics.presetDensity(rain) * 0.28f, 0.55f, 1.45f); + } + + static boolean playerSeesSky(Rain rain, Minecraft mc) { + if (!rain.skyCheck.get() || mc.player == null || mc.level == null) return true; + try { return mc.level.canSeeSky(mc.player.blockPosition()); } + catch (Throwable ignored) { return true; } + } + + static boolean isActiveWet(Rain rain) { + Minecraft mc = Minecraft.getInstance(); + return rain.isEnabled() && rain.wetGround.get() && rain.wetSurfaceDepthSupported + && mc.player != null && mc.level != null + && (!rain.onlyWhenRaining.get() || mc.level.isRaining()) + && playerSeesSky(rain, mc); + } + + static boolean renderWet(Rain rain, PostProcessContext ctx, GpuTextureView src, GpuTextureView dst) { + if (!isActiveWet(rain) || ctx == null || src == null || dst == null) return false; + GpuTextureView mainDepth = ctx.mainDepth(); + boolean hasDepth = mainDepth != null; + if (!hasDepth) mainDepth = src; + Matrix4f projection = LoVisualWorldMatrices.renderProjectionMatrix(); + if (projection == null) projection = RenderState.worldProjection; + rain.wetSurfaceProjection.set(projection); + rain.wetSurfaceInvView.set(new Matrix4f().rotation(RenderState.cameraRotation)); + WetSurfaceUniforms.update( + rain.wetSurfaceProjection, rain.wetSurfaceInvView, + (float) RenderState.cameraPos.x, (float) RenderState.cameraPos.y, (float) RenderState.cameraPos.z, + ctx.width() > 0 ? 1.0f / ctx.width() : 0.0f, ctx.height() > 0 ? 1.0f / ctx.height() : 0.0f, + rain.clock, hasDepth, rain.wetness.get(), rain.puddleCoverage.get(), + rain.reflectionStrength.get(), rain.puddleRipples.get() ? rain.rippleStrength.get() : 0.0f, + rain.reflectionDistance.get(), reflectionSteps(rain), rainAmount(rain), + rain.puddleRipples.get(), binarySteps(rain), 0.18f); + try { + FullScreenRenderer.ensureInit(); + FullScreenRenderer.begin("LoVisual WetSurface Pass").attachment(dst) + .pipeline(LoVisualRenderPipelines.WET_SURFACE).uniform("WetSurface", WetSurfaceUniforms.get()) + .sampler("u_Texture", src, PostProcessManager.getSampler()) + .sampler("u_MainDepth", mainDepth, PostProcessManager.getSampler()).end(); + } catch (Throwable t) { rain.wetSurfaceDepthSupported = false; return false; } + return true; + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/sky/AtmoDawnFog.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/sky/AtmoDawnFog.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/sky/AtmoDawnFog.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/sky/AtmoDawnFog.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/sky/ShaderSky.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/sky/ShaderSky.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/sky/ShaderSky.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/sky/ShaderSky.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/wings/WingShapes.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/wings/WingShapes.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/wings/WingShapes.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/wings/WingShapes.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/wings/Wings.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/wings/Wings.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/wings/Wings.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/wings/Wings.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/wings/WingsRenderer.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/wings/WingsRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/wings/WingsRenderer.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/wings/WingsRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/world/Gps.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/world/Gps.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/world/Gps.java rename to mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/world/Gps.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/phase/HudPhase.java b/mod/src/main/java/dev/loki/lovisual/features/module/phase/HudPhase.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/phase/HudPhase.java rename to mod/src/main/java/dev/loki/lovisual/features/module/phase/HudPhase.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/phase/ModuleStateListener.java b/mod/src/main/java/dev/loki/lovisual/features/module/phase/ModuleStateListener.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/phase/ModuleStateListener.java rename to mod/src/main/java/dev/loki/lovisual/features/module/phase/ModuleStateListener.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/phase/Notifier.java b/mod/src/main/java/dev/loki/lovisual/features/module/phase/Notifier.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/phase/Notifier.java rename to mod/src/main/java/dev/loki/lovisual/features/module/phase/Notifier.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/phase/WorldPhase.java b/mod/src/main/java/dev/loki/lovisual/features/module/phase/WorldPhase.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/phase/WorldPhase.java rename to mod/src/main/java/dev/loki/lovisual/features/module/phase/WorldPhase.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/relations/EntityFilters.java b/mod/src/main/java/dev/loki/lovisual/features/relations/EntityFilters.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/relations/EntityFilters.java rename to mod/src/main/java/dev/loki/lovisual/features/relations/EntityFilters.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/relations/PlayerRelations.java b/mod/src/main/java/dev/loki/lovisual/features/relations/PlayerRelations.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/relations/PlayerRelations.java rename to mod/src/main/java/dev/loki/lovisual/features/relations/PlayerRelations.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/relations/StaffData.java b/mod/src/main/java/dev/loki/lovisual/features/relations/StaffData.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/relations/StaffData.java rename to mod/src/main/java/dev/loki/lovisual/features/relations/StaffData.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/relations/StaffTracker.java b/mod/src/main/java/dev/loki/lovisual/features/relations/StaffTracker.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/relations/StaffTracker.java rename to mod/src/main/java/dev/loki/lovisual/features/relations/StaffTracker.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/relations/category/CategoryRules.java b/mod/src/main/java/dev/loki/lovisual/features/relations/category/CategoryRules.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/relations/category/CategoryRules.java rename to mod/src/main/java/dev/loki/lovisual/features/relations/category/CategoryRules.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/relations/category/CategoryService.java b/mod/src/main/java/dev/loki/lovisual/features/relations/category/CategoryService.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/relations/category/CategoryService.java rename to mod/src/main/java/dev/loki/lovisual/features/relations/category/CategoryService.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/relations/category/CategoryType.java b/mod/src/main/java/dev/loki/lovisual/features/relations/category/CategoryType.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/relations/category/CategoryType.java rename to mod/src/main/java/dev/loki/lovisual/features/relations/category/CategoryType.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/security/BackdoorProtection.java b/mod/src/main/java/dev/loki/lovisual/features/security/BackdoorProtection.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/security/BackdoorProtection.java rename to mod/src/main/java/dev/loki/lovisual/features/security/BackdoorProtection.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/security/FilteredTranslatableTextContent.java b/mod/src/main/java/dev/loki/lovisual/features/security/FilteredTranslatableTextContent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/security/FilteredTranslatableTextContent.java rename to mod/src/main/java/dev/loki/lovisual/features/security/FilteredTranslatableTextContent.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/theme/EditableClickGuiTheme.java b/mod/src/main/java/dev/loki/lovisual/features/theme/EditableClickGuiTheme.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/theme/EditableClickGuiTheme.java rename to mod/src/main/java/dev/loki/lovisual/features/theme/EditableClickGuiTheme.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/theme/Theme.java b/mod/src/main/java/dev/loki/lovisual/features/theme/Theme.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/theme/Theme.java rename to mod/src/main/java/dev/loki/lovisual/features/theme/Theme.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/theme/ThemeStore.java b/mod/src/main/java/dev/loki/lovisual/features/theme/ThemeStore.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/theme/ThemeStore.java rename to mod/src/main/java/dev/loki/lovisual/features/theme/ThemeStore.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/theme/Themes.java b/mod/src/main/java/dev/loki/lovisual/features/theme/Themes.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/theme/Themes.java rename to mod/src/main/java/dev/loki/lovisual/features/theme/Themes.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/theme/presets/ThemePresets1.java b/mod/src/main/java/dev/loki/lovisual/features/theme/presets/ThemePresets1.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/theme/presets/ThemePresets1.java rename to mod/src/main/java/dev/loki/lovisual/features/theme/presets/ThemePresets1.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/theme/presets/ThemePresets2.java b/mod/src/main/java/dev/loki/lovisual/features/theme/presets/ThemePresets2.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/theme/presets/ThemePresets2.java rename to mod/src/main/java/dev/loki/lovisual/features/theme/presets/ThemePresets2.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/theme/presets/ThemePresets3.java b/mod/src/main/java/dev/loki/lovisual/features/theme/presets/ThemePresets3.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/theme/presets/ThemePresets3.java rename to mod/src/main/java/dev/loki/lovisual/features/theme/presets/ThemePresets3.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixininterface/device/IGlBackendInfo.java b/mod/src/main/java/dev/loki/lovisual/mixininterface/device/IGlBackendInfo.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixininterface/device/IGlBackendInfo.java rename to mod/src/main/java/dev/loki/lovisual/mixininterface/device/IGlBackendInfo.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixininterface/device/IGpuDevice.java b/mod/src/main/java/dev/loki/lovisual/mixininterface/device/IGpuDevice.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixininterface/device/IGpuDevice.java rename to mod/src/main/java/dev/loki/lovisual/mixininterface/device/IGpuDevice.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixininterface/device/IMsaaDevice.java b/mod/src/main/java/dev/loki/lovisual/mixininterface/device/IMsaaDevice.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixininterface/device/IMsaaDevice.java rename to mod/src/main/java/dev/loki/lovisual/mixininterface/device/IMsaaDevice.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixininterface/device/IMsaaTexture.java b/mod/src/main/java/dev/loki/lovisual/mixininterface/device/IMsaaTexture.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixininterface/device/IMsaaTexture.java rename to mod/src/main/java/dev/loki/lovisual/mixininterface/device/IMsaaTexture.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixininterface/entity/IEntity.java b/mod/src/main/java/dev/loki/lovisual/mixininterface/entity/IEntity.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixininterface/entity/IEntity.java rename to mod/src/main/java/dev/loki/lovisual/mixininterface/entity/IEntity.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixininterface/entity/ILocalPlayer.java b/mod/src/main/java/dev/loki/lovisual/mixininterface/entity/ILocalPlayer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixininterface/entity/ILocalPlayer.java rename to mod/src/main/java/dev/loki/lovisual/mixininterface/entity/ILocalPlayer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixininterface/entity/IPlayerAttackCooldown.java b/mod/src/main/java/dev/loki/lovisual/mixininterface/entity/IPlayerAttackCooldown.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixininterface/entity/IPlayerAttackCooldown.java rename to mod/src/main/java/dev/loki/lovisual/mixininterface/entity/IPlayerAttackCooldown.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixininterface/entity/IRoundedHitbox.java b/mod/src/main/java/dev/loki/lovisual/mixininterface/entity/IRoundedHitbox.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixininterface/entity/IRoundedHitbox.java rename to mod/src/main/java/dev/loki/lovisual/mixininterface/entity/IRoundedHitbox.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixininterface/render/IGuiGraphics.java b/mod/src/main/java/dev/loki/lovisual/mixininterface/render/IGuiGraphics.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixininterface/render/IGuiGraphics.java rename to mod/src/main/java/dev/loki/lovisual/mixininterface/render/IGuiGraphics.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixininterface/render/IItemEntityRenderState.java b/mod/src/main/java/dev/loki/lovisual/mixininterface/render/IItemEntityRenderState.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixininterface/render/IItemEntityRenderState.java rename to mod/src/main/java/dev/loki/lovisual/mixininterface/render/IItemEntityRenderState.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixininterface/render/IRenderPipeline.java b/mod/src/main/java/dev/loki/lovisual/mixininterface/render/IRenderPipeline.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixininterface/render/IRenderPipeline.java rename to mod/src/main/java/dev/loki/lovisual/mixininterface/render/IRenderPipeline.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/LoVisualMixinPlugin.java b/mod/src/main/java/dev/loki/lovisual/mixins/LoVisualMixinPlugin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/LoVisualMixinPlugin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/LoVisualMixinPlugin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/blaze3d/GlTextureInvoker.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/blaze3d/GlTextureInvoker.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/blaze3d/GlTextureInvoker.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/blaze3d/GlTextureInvoker.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/blaze3d/GpuDeviceAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/blaze3d/GpuDeviceAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/blaze3d/GpuDeviceAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/blaze3d/GpuDeviceAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/blaze3d/ShaderLoaderDefinitionsAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/blaze3d/ShaderLoaderDefinitionsAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/blaze3d/ShaderLoaderDefinitionsAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/blaze3d/ShaderLoaderDefinitionsAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/blaze3d/ShaderSourceKeyAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/blaze3d/ShaderSourceKeyAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/blaze3d/ShaderSourceKeyAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/blaze3d/ShaderSourceKeyAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/chat/ChatInputSuggestorAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/chat/ChatInputSuggestorAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/chat/ChatInputSuggestorAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/chat/ChatInputSuggestorAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/chat/ChatScreenAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/chat/ChatScreenAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/chat/ChatScreenAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/chat/ChatScreenAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/chat/ChatScreenSuggestorAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/chat/ChatScreenSuggestorAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/chat/ChatScreenSuggestorAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/chat/ChatScreenSuggestorAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/chat/SuggestionWindowAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/chat/SuggestionWindowAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/chat/SuggestionWindowAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/chat/SuggestionWindowAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/entity/EntityAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/entity/EntityAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/entity/EntityAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/entity/EntityAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/entity/EntityInvoker.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/entity/EntityInvoker.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/entity/EntityInvoker.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/entity/EntityInvoker.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/entity/LivingEntityAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/entity/LivingEntityAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/entity/LivingEntityAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/entity/LivingEntityAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/entity/LocalPlayerAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/entity/LocalPlayerAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/entity/LocalPlayerAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/entity/LocalPlayerAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/hud/BossHealthOverlayAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/hud/BossHealthOverlayAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/hud/BossHealthOverlayAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/hud/BossHealthOverlayAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/hud/InGameOverlayRendererAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/hud/InGameOverlayRendererAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/hud/InGameOverlayRendererAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/hud/InGameOverlayRendererAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/hud/PlayerTabOverlayAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/hud/PlayerTabOverlayAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/hud/PlayerTabOverlayAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/hud/PlayerTabOverlayAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/hud/TextIconButtonWidgetAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/hud/TextIconButtonWidgetAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/hud/TextIconButtonWidgetAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/hud/TextIconButtonWidgetAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/inventory/ItemCooldownEntryAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/inventory/ItemCooldownEntryAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/inventory/ItemCooldownEntryAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/inventory/ItemCooldownEntryAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/inventory/ItemCooldownManagerAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/inventory/ItemCooldownManagerAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/inventory/ItemCooldownManagerAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/inventory/ItemCooldownManagerAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/inventory/PlayerInventoryAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/inventory/PlayerInventoryAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/inventory/PlayerInventoryAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/inventory/PlayerInventoryAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/inventory/SlotAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/inventory/SlotAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/inventory/SlotAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/inventory/SlotAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/misc/InputAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/misc/InputAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/misc/InputAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/misc/InputAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/misc/KeyMappingAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/misc/KeyMappingAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/misc/KeyMappingAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/misc/KeyMappingAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/misc/MinecraftAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/misc/MinecraftAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/misc/MinecraftAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/misc/MinecraftAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/misc/MultiPlayerGameModeAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/misc/MultiPlayerGameModeAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/misc/MultiPlayerGameModeAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/misc/MultiPlayerGameModeAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/packet/ClientboundExplodePacketAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/packet/ClientboundExplodePacketAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/packet/ClientboundExplodePacketAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/packet/ClientboundExplodePacketAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/packet/ClientboundSetEntityMotionPacketAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/packet/ClientboundSetEntityMotionPacketAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/packet/ClientboundSetEntityMotionPacketAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/packet/ClientboundSetEntityMotionPacketAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/packet/ServerboundInteractPacketAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/packet/ServerboundInteractPacketAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/packet/ServerboundInteractPacketAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/packet/ServerboundInteractPacketAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/packet/ServerboundMovePlayerPacketAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/packet/ServerboundMovePlayerPacketAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/packet/ServerboundMovePlayerPacketAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/packet/ServerboundMovePlayerPacketAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/projectile/PersistentProjectileEntityAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/projectile/PersistentProjectileEntityAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/projectile/PersistentProjectileEntityAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/projectile/PersistentProjectileEntityAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/renderer/EntityRenderManagerAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/renderer/EntityRenderManagerAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/renderer/EntityRenderManagerAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/renderer/EntityRenderManagerAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/renderer/EntityRendererAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/renderer/EntityRendererAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/renderer/EntityRendererAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/renderer/EntityRendererAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/renderer/GameRendererAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/renderer/GameRendererAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/renderer/GameRendererAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/renderer/GameRendererAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/renderer/RenderLayerInvokerAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/renderer/RenderLayerInvokerAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/renderer/RenderLayerInvokerAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/renderer/RenderLayerInvokerAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/state/EffectParticleEffectAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/state/EffectParticleEffectAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/state/EffectParticleEffectAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/state/EffectParticleEffectAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/state/FireworksComponentAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/state/FireworksComponentAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/state/FireworksComponentAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/state/FireworksComponentAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/state/LivingEntityRenderStateAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/state/LivingEntityRenderStateAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/state/LivingEntityRenderStateAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/state/LivingEntityRenderStateAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/widget/ClickableWidgetAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/widget/ClickableWidgetAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/widget/ClickableWidgetAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/widget/ClickableWidgetAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/widget/CyclingButtonWidgetAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/widget/CyclingButtonWidgetAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/widget/CyclingButtonWidgetAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/widget/CyclingButtonWidgetAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/widget/SliderWidgetAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/widget/SliderWidgetAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/widget/SliderWidgetAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/widget/SliderWidgetAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/widget/TextFieldWidgetAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/widget/TextFieldWidgetAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/widget/TextFieldWidgetAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/widget/TextFieldWidgetAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/world/ClientLevelAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/world/ClientLevelAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/world/ClientLevelAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/world/ClientLevelAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/world/LevelExtractorAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/world/LevelExtractorAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/world/LevelExtractorAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/world/LevelExtractorAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/world/LevelRendererAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/world/LevelRendererAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/world/LevelRendererAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/world/LevelRendererAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/world/WorldAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/accessors/world/WorldAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/accessors/world/WorldAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/accessors/world/WorldAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/blaze3d/device/GlDeviceBackendMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/device/GlDeviceBackendMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/blaze3d/device/GlDeviceBackendMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/device/GlDeviceBackendMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/blaze3d/device/GpuDeviceMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/device/GpuDeviceMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/blaze3d/device/GpuDeviceMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/device/GpuDeviceMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/blaze3d/device/GpuSurfaceMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/device/GpuSurfaceMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/blaze3d/device/GpuSurfaceMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/device/GpuSurfaceMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/blaze3d/device/VulkanDeviceMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/device/VulkanDeviceMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/blaze3d/device/VulkanDeviceMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/device/VulkanDeviceMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/blaze3d/encoder/BatchingRenderCommandQueueMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/encoder/BatchingRenderCommandQueueMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/blaze3d/encoder/BatchingRenderCommandQueueMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/encoder/BatchingRenderCommandQueueMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/blaze3d/encoder/CommandEncoderMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/encoder/CommandEncoderMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/blaze3d/encoder/CommandEncoderMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/encoder/CommandEncoderMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/blaze3d/encoder/VulkanCommandEncoderMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/encoder/VulkanCommandEncoderMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/blaze3d/encoder/VulkanCommandEncoderMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/encoder/VulkanCommandEncoderMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/blaze3d/pipeline/RenderPassMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/pipeline/RenderPassMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/blaze3d/pipeline/RenderPassMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/pipeline/RenderPassMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/blaze3d/pipeline/RenderPipelineMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/pipeline/RenderPipelineMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/blaze3d/pipeline/RenderPipelineMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/pipeline/RenderPipelineMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/blaze3d/pipeline/VulkanRenderPassMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/pipeline/VulkanRenderPassMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/blaze3d/pipeline/VulkanRenderPassMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/pipeline/VulkanRenderPassMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/blaze3d/pipeline/VulkanRenderPipelineMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/pipeline/VulkanRenderPipelineMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/blaze3d/pipeline/VulkanRenderPipelineMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/pipeline/VulkanRenderPipelineMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/blaze3d/shader/DynamicUniformStorageMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/shader/DynamicUniformStorageMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/blaze3d/shader/DynamicUniformStorageMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/shader/DynamicUniformStorageMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/blaze3d/shader/GlStateManagerMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/shader/GlStateManagerMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/blaze3d/shader/GlStateManagerMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/shader/GlStateManagerMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/blaze3d/shader/ShaderLoaderMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/shader/ShaderLoaderMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/blaze3d/shader/ShaderLoaderMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/shader/ShaderLoaderMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/blaze3d/texture/BufferManagerDefaultBufferManagerMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/texture/BufferManagerDefaultBufferManagerMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/blaze3d/texture/BufferManagerDefaultBufferManagerMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/texture/BufferManagerDefaultBufferManagerMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/blaze3d/texture/GlTextureMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/texture/GlTextureMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/blaze3d/texture/GlTextureMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/texture/GlTextureMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/blaze3d/texture/VulkanGpuTextureMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/texture/VulkanGpuTextureMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/blaze3d/texture/VulkanGpuTextureMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/texture/VulkanGpuTextureMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/blaze3d/texture/VulkanGpuTextureViewMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/texture/VulkanGpuTextureViewMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/blaze3d/texture/VulkanGpuTextureViewMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/blaze3d/texture/VulkanGpuTextureViewMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/entity/inventory/ItemCooldownsMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/entity/inventory/ItemCooldownsMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/entity/inventory/ItemCooldownsMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/entity/inventory/ItemCooldownsMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/entity/inventory/ItemMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/entity/inventory/ItemMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/entity/inventory/ItemMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/entity/inventory/ItemMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/entity/inventory/ItemStackMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/entity/inventory/ItemStackMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/entity/inventory/ItemStackMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/entity/inventory/ItemStackMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/entity/inventory/PlayerInventoryMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/entity/inventory/PlayerInventoryMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/entity/inventory/PlayerInventoryMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/entity/inventory/PlayerInventoryMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/entity/living/LivingEntityMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/entity/living/LivingEntityMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/entity/living/LivingEntityMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/entity/living/LivingEntityMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/entity/living/LivingEntityRendererMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/entity/living/LivingEntityRendererMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/entity/living/LivingEntityRendererMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/entity/living/LivingEntityRendererMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/entity/player/AbstractClientPlayerMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/entity/player/AbstractClientPlayerMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/entity/player/AbstractClientPlayerMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/entity/player/AbstractClientPlayerMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/entity/player/LocalPlayerDropMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/entity/player/LocalPlayerDropMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/entity/player/LocalPlayerDropMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/entity/player/LocalPlayerDropMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/entity/player/LocalPlayerMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/entity/player/LocalPlayerMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/entity/player/LocalPlayerMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/entity/player/LocalPlayerMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/entity/player/PlayerEntityMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/entity/player/PlayerEntityMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/entity/player/PlayerEntityMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/entity/player/PlayerEntityMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/entity/world/EntityMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/entity/world/EntityMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/entity/world/EntityMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/entity/world/EntityMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/entity/world/FireworkRocketEntityMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/entity/world/FireworkRocketEntityMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/entity/world/FireworkRocketEntityMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/entity/world/FireworkRocketEntityMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/entity/world/ProjectileUtilMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/entity/world/ProjectileUtilMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/entity/world/ProjectileUtilMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/entity/world/ProjectileUtilMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/chat/ChatComponentMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/gui/chat/ChatComponentMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/chat/ChatComponentMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/gui/chat/ChatComponentMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/chat/ChatScreenMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/gui/chat/ChatScreenMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/chat/ChatScreenMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/gui/chat/ChatScreenMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/chat/ContextualBarMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/gui/chat/ContextualBarMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/chat/ContextualBarMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/gui/chat/ContextualBarMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/core/AbstractContainerScreenMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/gui/core/AbstractContainerScreenMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/core/AbstractContainerScreenMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/gui/core/AbstractContainerScreenMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/core/AbstractSelectionListMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/gui/core/AbstractSelectionListMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/core/AbstractSelectionListMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/gui/core/AbstractSelectionListMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/core/AbstractSliderButtonMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/gui/core/AbstractSliderButtonMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/core/AbstractSliderButtonMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/gui/core/AbstractSliderButtonMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/core/AbstractWidgetMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/gui/core/AbstractWidgetMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/core/AbstractWidgetMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/gui/core/AbstractWidgetMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/multiplayer/JoinMultiplayerScreenMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/gui/multiplayer/JoinMultiplayerScreenMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/multiplayer/JoinMultiplayerScreenMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/gui/multiplayer/JoinMultiplayerScreenMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/multiplayer/RecipeBookScreenMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/gui/multiplayer/RecipeBookScreenMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/multiplayer/RecipeBookScreenMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/gui/multiplayer/RecipeBookScreenMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/screens/GuiScreenMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/gui/screens/GuiScreenMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/screens/GuiScreenMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/gui/screens/GuiScreenMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/screens/PauseScreenMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/gui/screens/PauseScreenMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/screens/PauseScreenMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/gui/screens/PauseScreenMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/screens/ScreenMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/gui/screens/ScreenMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/screens/ScreenMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/gui/screens/ScreenMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/screens/TitleScreenMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/gui/screens/TitleScreenMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/screens/TitleScreenMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/gui/screens/TitleScreenMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/widgets/ButtonWidgetTextMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/gui/widgets/ButtonWidgetTextMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/widgets/ButtonWidgetTextMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/gui/widgets/ButtonWidgetTextMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/widgets/CycleButtonMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/gui/widgets/CycleButtonMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/widgets/CycleButtonMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/gui/widgets/CycleButtonMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/widgets/SpriteIconButtonCenteredIconMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/gui/widgets/SpriteIconButtonCenteredIconMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/widgets/SpriteIconButtonCenteredIconMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/gui/widgets/SpriteIconButtonCenteredIconMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/widgets/SpriteIconButtonTextAndIconMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/gui/widgets/SpriteIconButtonTextAndIconMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/widgets/SpriteIconButtonTextAndIconMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/gui/widgets/SpriteIconButtonTextAndIconMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/hud/DebugHudMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/hud/DebugHudMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/hud/DebugHudMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/hud/DebugHudMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/hud/GuiGraphicsMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/hud/GuiGraphicsMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/hud/GuiGraphicsMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/hud/GuiGraphicsMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/hud/GuiMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/hud/GuiMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/hud/GuiMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/hud/GuiMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/hud/GuiRendererMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/hud/GuiRendererMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/hud/GuiRendererMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/hud/GuiRendererMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/hud/accessibility/NarratorManagerMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/hud/accessibility/NarratorManagerMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/hud/accessibility/NarratorManagerMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/hud/accessibility/NarratorManagerMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/hud/accessibility/OptionsNarratorMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/hud/accessibility/OptionsNarratorMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/hud/accessibility/OptionsNarratorMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/hud/accessibility/OptionsNarratorMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/hud/bars/BossHealthOverlayMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/hud/bars/BossHealthOverlayMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/hud/bars/BossHealthOverlayMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/hud/bars/BossHealthOverlayMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/hud/bars/ExperienceBarMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/hud/bars/ExperienceBarMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/hud/bars/ExperienceBarMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/hud/bars/ExperienceBarMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/hud/bars/JumpBarMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/hud/bars/JumpBarMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/hud/bars/JumpBarMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/hud/bars/JumpBarMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/hud/bars/LocatorBarMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/hud/bars/LocatorBarMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/hud/bars/LocatorBarMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/hud/bars/LocatorBarMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/hud/tablist/PlayerTabOverlayMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/hud/tablist/PlayerTabOverlayMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/hud/tablist/PlayerTabOverlayMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/hud/tablist/PlayerTabOverlayMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/hud/tooltips/ToastManagerMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/hud/tooltips/ToastManagerMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/hud/tooltips/ToastManagerMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/hud/tooltips/ToastManagerMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/hud/tooltips/TooltipRenderUtilMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/hud/tooltips/TooltipRenderUtilMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/hud/tooltips/TooltipRenderUtilMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/hud/tooltips/TooltipRenderUtilMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/input/keyboard/KeyMappingMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/input/keyboard/KeyMappingMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/input/keyboard/KeyMappingMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/input/keyboard/KeyMappingMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/input/keyboard/KeyboardInputMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/input/keyboard/KeyboardInputMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/input/keyboard/KeyboardInputMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/input/keyboard/KeyboardInputMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/input/keyboard/KeyboardMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/input/keyboard/KeyboardMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/input/keyboard/KeyboardMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/input/keyboard/KeyboardMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/input/mouse/InputMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/input/mouse/InputMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/input/mouse/InputMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/input/mouse/InputMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/input/mouse/MouseMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/input/mouse/MouseMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/input/mouse/MouseMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/input/mouse/MouseMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/iris/hand/IrisHandRendererAccessor.java b/mod/src/main/java/dev/loki/lovisual/mixins/iris/hand/IrisHandRendererAccessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/iris/hand/IrisHandRendererAccessor.java rename to mod/src/main/java/dev/loki/lovisual/mixins/iris/hand/IrisHandRendererAccessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/iris/hand/IrisHandRendererMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/iris/hand/IrisHandRendererMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/iris/hand/IrisHandRendererMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/iris/hand/IrisHandRendererMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/iris/hand/IrisVanillaHandInteropMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/iris/hand/IrisVanillaHandInteropMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/iris/hand/IrisVanillaHandInteropMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/iris/hand/IrisVanillaHandInteropMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/iris/processor/IrisIncludeProcessorMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/iris/processor/IrisIncludeProcessorMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/iris/processor/IrisIncludeProcessorMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/iris/processor/IrisIncludeProcessorMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/iris/render/IrisCommonUniformsMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/iris/render/IrisCommonUniformsMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/iris/render/IrisCommonUniformsMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/iris/render/IrisCommonUniformsMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/iris/render/IrisGameRendererInteropMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/iris/render/IrisGameRendererInteropMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/iris/render/IrisGameRendererInteropMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/iris/render/IrisGameRendererInteropMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/iris/render/IrisRenderingPipelineFinalizeMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/iris/render/IrisRenderingPipelineFinalizeMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/iris/render/IrisRenderingPipelineFinalizeMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/iris/render/IrisRenderingPipelineFinalizeMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/iris/render/IrisShaderPackLoadMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/iris/render/IrisShaderPackLoadMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/iris/render/IrisShaderPackLoadMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/iris/render/IrisShaderPackLoadMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/moreculling/CullingUtilsMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/moreculling/CullingUtilsMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/moreculling/CullingUtilsMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/moreculling/CullingUtilsMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/network/game/ClientPacketListenerMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/network/game/ClientPacketListenerMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/network/game/ClientPacketListenerMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/network/game/ClientPacketListenerMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/network/game/MultiPlayerGameModeMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/network/game/MultiPlayerGameModeMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/network/game/MultiPlayerGameModeMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/network/game/MultiPlayerGameModeMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/network/game/MultiPlayerGameModeThrowMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/network/game/MultiPlayerGameModeThrowMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/network/game/MultiPlayerGameModeThrowMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/network/game/MultiPlayerGameModeThrowMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/network/game/PlayerInfoMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/network/game/PlayerInfoMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/network/game/PlayerInfoMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/network/game/PlayerInfoMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/network/packet/ClientCommonPacketListenerImplMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/network/packet/ClientCommonPacketListenerImplMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/network/packet/ClientCommonPacketListenerImplMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/network/packet/ClientCommonPacketListenerImplMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/network/packet/ConnectionMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/network/packet/ConnectionMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/network/packet/ConnectionMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/network/packet/ConnectionMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/network/packet/ServerboundUseItemPacketMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/network/packet/ServerboundUseItemPacketMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/network/packet/ServerboundUseItemPacketMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/network/packet/ServerboundUseItemPacketMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/entity/EntityRenderDispatcherMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/entity/EntityRenderDispatcherMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/entity/EntityRenderDispatcherMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/render/entity/EntityRenderDispatcherMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/entity/LivingEntityRenderStateMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/entity/LivingEntityRenderStateMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/entity/LivingEntityRenderStateMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/render/entity/LivingEntityRenderStateMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/entity/crystal/EndCrystalModelMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/entity/crystal/EndCrystalModelMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/entity/crystal/EndCrystalModelMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/render/entity/crystal/EndCrystalModelMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/entity/crystal/EndCrystalModelTintMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/entity/crystal/EndCrystalModelTintMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/entity/crystal/EndCrystalModelTintMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/render/entity/crystal/EndCrystalModelTintMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/entity/item/ItemEntityRenderStateMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/entity/item/ItemEntityRenderStateMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/entity/item/ItemEntityRenderStateMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/render/entity/item/ItemEntityRenderStateMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/entity/item/ItemEntityRendererMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/entity/item/ItemEntityRendererMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/entity/item/ItemEntityRendererMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/render/entity/item/ItemEntityRendererMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/fog/AtmosphericFogEnvironmentMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/fog/AtmosphericFogEnvironmentMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/fog/AtmosphericFogEnvironmentMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/render/fog/AtmosphericFogEnvironmentMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/fog/FogRendererMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/fog/FogRendererMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/fog/FogRendererMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/render/fog/FogRendererMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/fog/LavaFogModifierMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/fog/LavaFogModifierMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/fog/LavaFogModifierMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/render/fog/LavaFogModifierMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/fog/WaterFogEnvironmentMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/fog/WaterFogEnvironmentMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/fog/WaterFogEnvironmentMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/render/fog/WaterFogEnvironmentMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/fogmod/PowderSnowFogModifierMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/fogmod/PowderSnowFogModifierMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/fogmod/PowderSnowFogModifierMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/render/fogmod/PowderSnowFogModifierMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/item/EquipmentLayerRendererMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/item/EquipmentLayerRendererMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/item/EquipmentLayerRendererMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/render/item/EquipmentLayerRendererMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/item/ItemFeatureRendererMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/item/ItemFeatureRendererMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/item/ItemFeatureRendererMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/render/item/ItemFeatureRendererMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/item/ItemInHandAlwaysVisibleMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/item/ItemInHandAlwaysVisibleMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/item/ItemInHandAlwaysVisibleMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/render/item/ItemInHandAlwaysVisibleMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/item/ItemInHandRendererMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/item/ItemInHandRendererMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/item/ItemInHandRendererMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/render/item/ItemInHandRendererMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/level/CameraMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/level/CameraMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/level/CameraMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/render/level/CameraMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/level/LevelRendererMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/level/LevelRendererMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/level/LevelRendererMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/render/level/LevelRendererMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/level/SkyRendererMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/level/SkyRendererMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/level/SkyRendererMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/render/level/SkyRendererMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/level/particles/FireworkParticlesStarterMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/level/particles/FireworkParticlesStarterMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/level/particles/FireworkParticlesStarterMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/render/level/particles/FireworkParticlesStarterMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/level/particles/ParticleManagerMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/level/particles/ParticleManagerMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/level/particles/ParticleManagerMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/render/level/particles/ParticleManagerMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/light/LightmapMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/light/LightmapMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/light/LightmapMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/render/light/LightmapMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/light/LightmapRenderStateExtractorMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/light/LightmapRenderStateExtractorMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/light/LightmapRenderStateExtractorMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/render/light/LightmapRenderStateExtractorMixin.java diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/main/GameRendererMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/main/GameRendererMixin.java new file mode 100644 index 0000000..a052d8a --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/main/GameRendererMixin.java @@ -0,0 +1,344 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.mixins.render.main; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.mojang.blaze3d.buffers.GpuBufferSlice; +import com.mojang.blaze3d.pipeline.RenderTarget; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.Camera; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.AbstractClientPlayer; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.client.renderer.ProjectionMatrixBuffer; +import net.minecraft.client.renderer.SubmitNodeStorage; +import net.minecraft.client.renderer.feature.FeatureRenderDispatcher; +import net.minecraft.client.renderer.state.GameRenderState; +import net.minecraft.client.renderer.state.level.CameraRenderState; +import net.minecraft.util.Mth; +import org.joml.Matrix4f; +import org.joml.Matrix4fc; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import dev.loki.lovisual.config.i18n.MainConfig; +import dev.loki.lovisual.features.gui.hud.nondraggable.impl.buttons.BetterButtons; +import dev.loki.lovisual.features.module.lifecycle.Modules; +import dev.loki.lovisual.features.module.modules.visuals.camera.AspectRatio; +import dev.loki.lovisual.features.module.modules.visuals.camera.Freecam; +import dev.loki.lovisual.features.module.modules.visuals.camera.ViewModel; +import dev.loki.lovisual.features.module.modules.visuals.scene.visual.Chams; +import dev.loki.lovisual.mixins.accessors.renderer.GameRendererAccessor; +import dev.loki.lovisual.render.engine.core.system.LoVisualWorldMatrices; +import dev.loki.lovisual.render.engine.msaa.MsaaWorldTarget; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.iris.state.IrisLoVisualFrameHooks; +import dev.loki.lovisual.render.iris.scene.IrisFinalizedSceneRenderer; +import dev.loki.lovisual.util.screen.ClientScreen; + +/** + * Hook-only mixin: engine bodies live in handlers/ (GameRendererWorldEngine, + * GameRendererWorldPost, GameRendererWorldDebug, GameRendererProjectionHelper). + */ +@Mixin(GameRenderer.class) +public abstract class GameRendererMixin + implements IrisFinalizedSceneRenderer, GameRendererProjectionHelper.BobInvoker, GameRendererWorldEngine.Host { + + @Unique + private final GameRendererProjectionHelper lovisual$projectionHelper = new GameRendererProjectionHelper(); + @Unique + private final GameRendererWorldEngine lovisual$worldEngine = new GameRendererWorldEngine(); + @Unique + private final GameRendererWorldPost lovisual$worldPost = new GameRendererWorldPost(lovisual$worldEngine); + // Per-frame scratch (GameRenderer is a singleton; consumers copy by value). + @Unique + private final PoseStack lovisual$effectMatrices = new PoseStack(); + @Unique + private final Matrix4f lovisual$screenProjectionScratch = new Matrix4f(); + + @Shadow + @Final + private Minecraft minecraft; + @Shadow + @Final + private Camera mainCamera; + @Shadow + @Final + private GameRenderState gameRenderState; + @Shadow + private float spinningEffectTime; + @Shadow + private float spinningEffectSpeed; + + @Shadow + protected abstract void bobView(CameraRenderState cameraRenderState, PoseStack matrices); + + @Shadow + protected abstract void bobHurt(CameraRenderState cameraRenderState, PoseStack matrices); + + @Unique + @Override + public Minecraft lovisual$minecraft() { + return minecraft; + } + + @Unique + @Override + public Camera lovisual$mainCamera() { + return mainCamera; + } + + @Unique + @Override + public GameRendererProjectionHelper lovisual$projection() { + return lovisual$projectionHelper; + } + + @Unique + @Override + public float lovisual$spinningTime() { + return spinningEffectTime; + } + + @Unique + @Override + public float lovisual$spinningSpeed() { + return spinningEffectSpeed; + } + + @Unique + @Override + public void lovisual$bobHurt(CameraRenderState cameraRenderState, PoseStack matrices) { + bobHurt(cameraRenderState, matrices); + } + + @Unique + @Override + public void lovisual$bobView(CameraRenderState cameraRenderState, PoseStack matrices) { + bobView(cameraRenderState, matrices); + } + + @Inject(method = "tick", at = @At("HEAD"), cancellable = true) + private void lovisual$skipTickWithoutPlayer(CallbackInfo ci) { + if (minecraft != null && minecraft.level != null && minecraft.player == null) { + ci.cancel(); + } + } + + @Inject(method = "extract(Lnet/minecraft/client/DeltaTracker;Z)V", at = @At("HEAD")) + private void lovisual$beginDeferred2DExtract(DeltaTracker tickCounter, boolean tick, CallbackInfo ci) { + Renderer2D.beginDeferredExtractFrame(); + } + + @Inject(method = "extract(Lnet/minecraft/client/DeltaTracker;Z)V", at = @At("TAIL")) + private void lovisual$endDeferred2DExtract(DeltaTracker tickCounter, boolean tick, CallbackInfo ci) { + Renderer2D.endDeferredExtractFrame(); + } + + @Redirect( + method = "renderItemInHand(Lnet/minecraft/client/renderer/state/level/CameraRenderState;FLorg/joml/Matrix4fc;)V", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/renderer/GameRenderer;bobView(Lnet/minecraft/client/renderer/state/level/CameraRenderState;Lcom/mojang/blaze3d/vertex/PoseStack;)V" + ) + ) + private void lovisual$viewModel$replaceHandBobbing(GameRenderer instance, + CameraRenderState cameraRenderState, + PoseStack matrices, + CameraRenderState originalCameraRenderState, + float tickDelta, + Matrix4fc positionMatrix) { + ViewModel viewModel = Modules.get(ViewModel.class); + if (viewModel != null && viewModel.smoothRunLoweringEnabled()) { + if (minecraft != null && minecraft.getCameraEntity() instanceof AbstractClientPlayer player) { + float movement = player.avatarState().getInterpolatedBob(tickDelta); + float yOffset = -Mth.clamp(movement, 0.0f, 1.0f) * viewModel.getSmoothRunLoweringAmount(); + if (Math.abs(yOffset) > 0.0001f) { + matrices.translate(0.0f, yOffset, 0.0f); + } + } + return; + } + + ((GameRendererAccessor) instance).invokeBobView(cameraRenderState, matrices); + } + + @Inject(method = "extractCamera", at = @At("TAIL")) + private void lovisual$modifyAspect(DeltaTracker tickCounter, float tickDelta, float fov, CallbackInfo ci) { + lovisual$projectionHelper.applyAspect(gameRenderState.levelRenderState.cameraRenderState); + } + + @Inject(method = "extractCamera", at = @At("TAIL")) + private void lovisual$captureProjectionFor2DOverlays(DeltaTracker tickCounter, float tickDelta, float fov, CallbackInfo ci) { + if (gameRenderState == null || gameRenderState.levelRenderState == null) return; + CameraRenderState cameraRenderState = gameRenderState.levelRenderState.cameraRenderState; + if (cameraRenderState == null || cameraRenderState.viewRotationMatrix == null || cameraRenderState.projectionMatrix == null) return; + + lovisual$effectMatrices.setIdentity(); + Matrix4f renderProjection = lovisual$projectionHelper.buildWorldProjection( + mainCamera, fov, lovisual$effectMatrices, tickDelta, + spinningEffectTime, spinningEffectSpeed, minecraft, this); + AspectRatio.applyToProjection(renderProjection); + Matrix4f screenProjection = lovisual$screenProjectionScratch.set(cameraRenderState.projectionMatrix); + LoVisualWorldMatrices.capture( + cameraRenderState.viewRotationMatrix, + renderProjection, + screenProjection, + cameraRenderState.pos != null ? cameraRenderState.pos : mainCamera.position() + ); + } + + @Inject(method = "mainRenderTarget", at = @At("HEAD"), cancellable = true) + private void lovisual$overrideWorldFramebuffer(CallbackInfoReturnable cir) { + RenderTarget msaa = MsaaWorldTarget.getFramebufferOverride(); + if (msaa != null) { + cir.setReturnValue(msaa); + } + } + + @Inject(method = "renderLevel", at = @At("HEAD")) + private void lovisual$beginWorldMsaa(DeltaTracker tickCounter, CallbackInfo ci) { + IrisLoVisualFrameHooks.beginRenderLevel(tickCounter); + LoVisualWorldMatrices.reset(); + MsaaWorldTarget.begin(minecraft, MainConfig.get().getMsaa3dSamples(), lovisual$projectionHelper.needsResolvedMainDepth()); + } + + @Inject(method = "renderLevel", at = @At("RETURN")) + private void lovisual$endIrisFrameHooks(DeltaTracker tickCounter, CallbackInfo ci) { + IrisLoVisualFrameHooks.endRenderLevel(tickCounter); + } + + @WrapOperation( + method = "renderLevel", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/renderer/ProjectionMatrixBuffer;getBuffer(Lorg/joml/Matrix4f;)Lcom/mojang/blaze3d/buffers/GpuBufferSlice;" + ) + ) + private GpuBufferSlice lovisual$captureFullWorldMatrices(ProjectionMatrixBuffer instance, + Matrix4f renderProjectionMatrix, + Operation original) { + if (renderProjectionMatrix != null + && gameRenderState != null + && gameRenderState.levelRenderState != null + && gameRenderState.levelRenderState.cameraRenderState != null + && gameRenderState.levelRenderState.cameraRenderState.viewRotationMatrix != null + && gameRenderState.levelRenderState.cameraRenderState.projectionMatrix != null) { + CameraRenderState cameraRenderState = gameRenderState.levelRenderState.cameraRenderState; + LoVisualWorldMatrices.capture( + cameraRenderState.viewRotationMatrix, + renderProjectionMatrix, + cameraRenderState.projectionMatrix, + cameraRenderState.pos != null ? cameraRenderState.pos : mainCamera.position() + ); + } + return original.call(instance, renderProjectionMatrix); + } + + @Inject( + method = "renderLevel", + at = @At( + value = "INVOKE_STRING", + target = "Lnet/minecraft/util/profiling/ProfilerFiller;popPush(Ljava/lang/String;)V", + args = "ldc=hand", + shift = At.Shift.AFTER + ) + ) + private void lovisual$postProcess(DeltaTracker tickCounter, CallbackInfo ci) { + lovisual$worldPost.preHand(this, tickCounter); + } + + @Override + public void lovisual$renderAfterIrisFinalization(DeltaTracker tickCounter) { + lovisual$worldPost.preHand(this, tickCounter); + } + + @Inject( + method = "renderLevel", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/renderer/feature/FeatureRenderDispatcher;renderAllFeatures(Lnet/minecraft/client/renderer/SubmitNodeStorage;)V", + shift = At.Shift.AFTER + ) + ) + private void lovisual$postProcessAfterHand(DeltaTracker tickCounter, CallbackInfo ci) { + lovisual$worldPost.afterHand(this, tickCounter); + } + + @Inject(method = "renderItemInHand(Lnet/minecraft/client/renderer/state/level/CameraRenderState;FLorg/joml/Matrix4fc;)V", at = @At("HEAD"), cancellable = true) + private void freecam$hand(CameraRenderState cameraRenderState, float tickDelta, Matrix4fc positionMatrix, CallbackInfo ci) { + Freecam fc = Modules.get(Freecam.class); + if (fc != null && fc.isEnabled() && !fc.renderHand()) { + ci.cancel(); + } + } + + @WrapOperation( + method = "renderItemInHand(Lnet/minecraft/client/renderer/state/level/CameraRenderState;FLorg/joml/Matrix4fc;)V", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/renderer/feature/FeatureRenderDispatcher;renderAllFeatures(Lnet/minecraft/client/renderer/SubmitNodeStorage;)V" + ) + ) + private void lovisual$renderChamsHandPreparedFrame(FeatureRenderDispatcher dispatcher, + SubmitNodeStorage storage, + Operation original, + CameraRenderState cameraRenderState, + float tickDelta, + Matrix4fc positionMatrix) { + Chams module = Modules.get(Chams.class); + if (module != null && module.renderPreparedHandScene(dispatcher, storage)) { + return; + } + original.call(dispatcher, storage); + } + + @Inject(method = "render(Lnet/minecraft/client/DeltaTracker;Z)V", at = @At("TAIL")) + private void lovisual$flushQueuedButtons(DeltaTracker tickCounter, boolean tick, CallbackInfo ci) { + Minecraft mc = Minecraft.getInstance(); + if (mc == null || ClientScreen.current() == null) return; + if (!BetterButtons.hasPending()) { + BetterButtons.renderTooltip(); + return; + } + BetterButtons.flush(); + BetterButtons.renderTooltip(); + } + + @Inject( + method = "render(Lnet/minecraft/client/DeltaTracker;Z)V", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/gui/render/GuiRenderer;render()V", + shift = At.Shift.BEFORE + ) + ) + private void lovisual$captureWorldForHudGlass(DeltaTracker tickCounter, boolean tick, CallbackInfo ci) { + lovisual$worldPost.beforeVanillaGui(minecraft, tickCounter); + } + + @Inject( + method = "render(Lnet/minecraft/client/DeltaTracker;Z)V", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/gui/render/GuiRenderer;render()V", + shift = At.Shift.AFTER + ) + ) + private void lovisual$debug2dImmediateAfterGui(DeltaTracker tickCounter, boolean tick, CallbackInfo ci) { + lovisual$worldPost.afterVanillaGui(tickCounter); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/main/GameRendererViewEffectsMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/main/GameRendererViewEffectsMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/main/GameRendererViewEffectsMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/render/main/GameRendererViewEffectsMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/main/MinecraftMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/main/MinecraftMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/main/MinecraftMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/render/main/MinecraftMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/main/ScreenEffectRendererMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/main/ScreenEffectRendererMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/main/ScreenEffectRendererMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/render/main/ScreenEffectRendererMixin.java diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/main/handlers/GameRendererProjectionHelper.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/main/handlers/GameRendererProjectionHelper.java new file mode 100644 index 0000000..6017fa6 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/main/handlers/GameRendererProjectionHelper.java @@ -0,0 +1,119 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.mixins.render.main; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.Camera; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.client.renderer.state.level.CameraRenderState; +import net.minecraft.util.Mth; +import net.minecraft.world.effect.MobEffects; +import net.minecraft.world.level.material.FogType; +import dev.loki.lovisual.config.i18n.MainConfig; +import dev.loki.lovisual.features.module.lifecycle.Modules; +import dev.loki.lovisual.features.module.modules.visuals.camera.AspectRatio; +import dev.loki.lovisual.features.module.modules.visuals.camera.Freecam; +import dev.loki.lovisual.features.module.modules.visuals.scene.visual.ReimaginedVisual; +import dev.loki.lovisual.features.module.modules.visuals.visibility.NoRender; +import dev.loki.lovisual.mixins.accessors.entity.LocalPlayerAccessor; +import org.joml.Matrix4f; +import org.joml.Vector3f; + +/** + * Projection math extracted from GameRendererMixin: aspect ratio, view bobbing, + * nausea and MSAA depth queries. Holds per-frame scratch matrices only. + */ +final class GameRendererProjectionHelper { + + private final Matrix4f inverseBobScratch = new Matrix4f(); + private final org.joml.Quaternionf conjugateScratch = new org.joml.Quaternionf(); + + interface BobInvoker { + void lovisual$bobHurt(CameraRenderState cameraRenderState, PoseStack matrices); + + void lovisual$bobView(CameraRenderState cameraRenderState, PoseStack matrices); + } + + void applyAspect(CameraRenderState state) { + AspectRatio ar = Modules.get(AspectRatio.class); + if (ar == null || !ar.isEnabled()) return; + float mul = ar.getMultiplier(); + if (mul == 1.0f || state == null || state.projectionMatrix == null) return; + // horizontal aspect stretch + state.projectionMatrix.m00(state.projectionMatrix.m00() * mul); + } + + Matrix4f buildWorldProjection(Camera camera, float fov, PoseStack viewEffects, float tickDelta, + float spinningTime, float spinningSpeed, + Minecraft minecraft, BobInvoker bobInvoker) { + CameraRenderState tmp = new CameraRenderState(); + camera.extractRenderState(tmp, fov); + Matrix4f projection = new Matrix4f(tmp.projectionMatrix); + applyViewEffects(viewEffects, tickDelta, camera, minecraft, bobInvoker); + projection.mul(viewEffects.last().pose()); + applyNausea(projection, tickDelta, minecraft, spinningTime, spinningSpeed); + return projection; + } + + private void applyViewEffects(PoseStack matrices, float tickDelta, Camera camera, + Minecraft minecraft, BobInvoker bobInvoker) { + NoRender noRender = Modules.get(NoRender.class); + CameraRenderState state = new CameraRenderState(); + camera.extractRenderState(state, camera.getFov()); + if (noRender == null || !noRender.off("camera_shake")) { + bobInvoker.lovisual$bobHurt(state, matrices); + } + boolean freecam = Modules.get(Freecam.class) != null && Modules.get(Freecam.class).isEnabled(); + if (minecraft.options.bobView().get() && !freecam + && (noRender == null || !noRender.off("view_bob"))) { + bobInvoker.lovisual$bobView(state, matrices); + } + } + + private void applyNausea(Matrix4f projection, float tickDelta, Minecraft minecraft, + float spinningTime, float spinningSpeed) { + LocalPlayer player = minecraft.player; + if (player == null) return; + float scale = minecraft.options.screenEffectScale().get().floatValue(); + if (scale <= 0.0f) return; + float intensity = 0.0f; + if (player instanceof LocalPlayerAccessor acc) { + intensity = Mth.lerp(tickDelta, acc.lovisual$getLastNauseaIntensity(), acc.lovisual$getNauseaIntensity()); + } + float effect = player.getEffectBlendFactor(MobEffects.NAUSEA, tickDelta); + float strength = Math.max(intensity, effect) * (scale * scale); + if (strength <= 0.0f) return; + float s = 5.0f / (strength * strength + 5.0f) - strength * 0.04f; + s *= s; + Vector3f axis = new Vector3f(0.0f, Mth.SQRT_OF_TWO / 2.0f, Mth.SQRT_OF_TWO / 2.0f); + float angle = (spinningTime + tickDelta * spinningSpeed) * Mth.DEG_TO_RAD; + projection.rotate(angle, axis); + projection.scale(1.0f / s, 1.0f, 1.0f); + projection.rotate(-angle, axis); + } + + boolean isWorldTranslucent(FogType type) { + return type == FogType.WATER || type == FogType.LAVA || type == FogType.POWDER_SNOW; + } + + boolean needsResolvedMainDepth() { + ReimaginedVisual module = Modules.get(ReimaginedVisual.class); + if (module == null) return false; + if (module.needsResolvedMainDepthCapture()) return true; + return MainConfig.get().getMsaa3dSamples() > 1 && module.isActive(); + } + + Matrix4f inverseBobScratch() { + return inverseBobScratch; + } + + org.joml.Quaternionf conjugateScratch() { + return conjugateScratch; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/main/handlers/GameRendererWorldDebug.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/main/handlers/GameRendererWorldDebug.java new file mode 100644 index 0000000..8cf0816 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/main/handlers/GameRendererWorldDebug.java @@ -0,0 +1,119 @@ +package dev.loki.lovisual.mixins.render.main; + +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.Camera; +import net.minecraft.util.Mth; +import net.minecraft.world.phys.Vec3; +import dev.loki.lovisual.render.engine.RenderState; +import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; +import dev.loki.lovisual.render.engine.renderer.MeshRenderer; +import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import dev.loki.lovisual.util.logging.DebugLog; +import org.joml.Matrix4f; + +/** + * Manual-only render debug probes (compile-time disabled). Extracted from + * GameRendererMixin to keep the mixin hook-only. + */ +final class GameRendererWorldDebug { + + private static final boolean CLIP_SPACE = false; + private static final boolean PRIMITIVE = false; + private static final long INTERVAL_MS = 2000L; + + private Renderer3D clipSpaceRenderer; + private long lastPrimitiveLogMs; + + /** Renders the NDC clip-space probe; when it ran the caller must return early. */ + boolean clipSpaceIfNeeded() { + if (!CLIP_SPACE) return false; + var mv = RenderSystem.getModelViewStack(); + mv.pushMatrix(); + mv.identity(); + + MeshRenderer.setProjection(new Matrix4f().identity()); + RenderState.rendering3D = false; + + if (clipSpaceRenderer == null) { + clipSpaceRenderer = new Renderer3D( + LoVisualRenderPipelines.WORLD_COLORED_LINES, + LoVisualRenderPipelines.WORLD_COLORED + ); + } + + clipSpaceRenderer.begin(); + + // Clip-space square + diagonals (NDC) + clipSpaceRenderer.line(-0.8, -0.8, 0.0, 0.8, -0.8, 0.0, 255, 0, 0, 255); + clipSpaceRenderer.line(0.8, -0.8, 0.0, 0.8, 0.8, 0.0, 0, 255, 0, 255); + clipSpaceRenderer.line(0.8, 0.8, 0.0, -0.8, 0.8, 0.0, 0, 0, 255, 255); + clipSpaceRenderer.line(-0.8, 0.8, 0.0, -0.8, -0.8, 0.0, 255, 255, 0, 255); + clipSpaceRenderer.line(-0.8, -0.8, 0.0, 0.8, 0.8, 0.0, 255, 0, 255, 255); + clipSpaceRenderer.line(0.8, -0.8, 0.0, -0.8, 0.8, 0.0, 0, 255, 255, 255); + + clipSpaceRenderer.render(new PoseStack()); + + mv.popMatrix(); + RenderState.rendering3D = false; + return true; + } + + /** Draws wire cubes 3 blocks ahead of the camera plus a throttled matrix diag log. */ + void primitiveIfNeeded(Renderer3D renderer, Renderer3D depthRenderer, Camera camera, Matrix4f projection) { + if (!PRIMITIVE) return; + Vec3 camPos = camera.position(); + float yaw = camera.yRot() * ((float) Math.PI / 180.0f); + float pitch = camera.xRot() * ((float) Math.PI / 180.0f); + Vec3 forward = new Vec3( + -Mth.sin(yaw) * Mth.cos(pitch), + -Mth.sin(pitch), + Mth.cos(yaw) * Mth.cos(pitch) + ); + + Vec3 center = camPos.add(forward.scale(3.0)); + double r = 0.5; + double x1 = center.x - r, x2 = center.x + r; + double y1 = center.y - r, y2 = center.y + r; + double z1 = center.z - r, z2 = center.z + r; + double ox1 = -1.0, oy1 = -1.0, oz1 = -1.0; + double ox2 = 1.0, oy2 = 1.0, oz2 = 1.0; + + // Wire cube (no depth) and depth variant + Renderer3D[] targets = new Renderer3D[]{renderer, depthRenderer}; + for (Renderer3D rdr : targets) { + drawDebugCube(rdr, x1, y1, z1, x2, y2, z2); + drawDebugCube(rdr, ox1, oy1, oz1, ox2, oy2, oz2); + } + + long now = System.currentTimeMillis(); + if (now - lastPrimitiveLogMs >= INTERVAL_MS) { + lastPrimitiveLogMs = now; + Matrix4f mvMat = RenderSystem.getModelViewMatrixCopy(); + DebugLog.renderThread( + "[LoVisual] Debug diag: cam=" + camPos + + " proj=[" + projection.m00() + "," + projection.m11() + "," + projection.m22() + "," + projection.m33() + "]" + + " mv=[" + mvMat.m30() + "," + mvMat.m31() + "," + mvMat.m32() + "]" + ); + } + } + + private static void drawDebugCube(Renderer3D rdr, + double x1, double y1, double z1, + double x2, double y2, double z2) { + rdr.line(x1, y1, z1, x2, y1, z1, 255, 0, 0, 255); + rdr.line(x1, y2, z1, x2, y2, z1, 255, 0, 0, 255); + rdr.line(x1, y1, z2, x2, y1, z2, 255, 0, 0, 255); + rdr.line(x1, y2, z2, x2, y2, z2, 255, 0, 0, 255); + + rdr.line(x1, y1, z1, x1, y2, z1, 0, 255, 0, 255); + rdr.line(x2, y1, z1, x2, y2, z1, 0, 255, 0, 255); + rdr.line(x1, y1, z2, x1, y2, z2, 0, 255, 0, 255); + rdr.line(x2, y1, z2, x2, y2, z2, 0, 255, 0, 255); + + rdr.line(x1, y1, z1, x1, y1, z2, 0, 0, 255, 255); + rdr.line(x2, y1, z1, x2, y1, z2, 0, 0, 255, 255); + rdr.line(x1, y2, z1, x1, y2, z2, 0, 0, 255, 255); + rdr.line(x2, y2, z1, x2, y2, z2, 0, 0, 255, 255); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/main/handlers/GameRendererWorldEngine.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/main/handlers/GameRendererWorldEngine.java new file mode 100644 index 0000000..5bb2c25 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/main/handlers/GameRendererWorldEngine.java @@ -0,0 +1,200 @@ +package dev.loki.lovisual.mixins.render.main; + +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.Camera; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.culling.Frustum; +import net.minecraft.client.renderer.state.level.CameraRenderState; +import net.minecraft.world.phys.Vec3; +import dev.loki.lovisual.addon.pipeline.AddonRenderPipelineManager; +import dev.loki.lovisual.api.v0.render.LoVisualRenderStage; +import dev.loki.lovisual.features.module.core.ModuleManager; +import dev.loki.lovisual.features.module.phase.WorldPhase; +import dev.loki.lovisual.render.engine.RenderState; +import dev.loki.lovisual.render.engine.core.frame.RenderPhase; +import dev.loki.lovisual.render.engine.core.frame.RenderPhaseScope; +import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; +import dev.loki.lovisual.render.engine.core.system.LoVisualWorldMatrices; +import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; +import dev.loki.lovisual.render.engine.profiler.bridge.TracyGpuProfiler; +import dev.loki.lovisual.render.engine.profiler.config.ProfilerPhase; +import dev.loki.lovisual.render.engine.profiler.render.RenderProfiler3D; +import dev.loki.lovisual.render.engine.renderer.MeshRenderer; +import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import dev.loki.lovisual.render.iris.state.IrisLoVisualFrameHooks; +import org.joml.Matrix4f; +import org.joml.Matrix4fStack; + +/** + * World-render engine bodies extracted from GameRendererMixin: the + * BEFORE_TRANSLUCENT/END_MAIN world pass and the post-process world pass. + */ +final class GameRendererWorldEngine { + + private final GameRendererWorldDebug debug = new GameRendererWorldDebug(); + private final PoseStack matrices = new PoseStack(); + private final PoseStack postMatrices = new PoseStack(); + private final Matrix4f cameraPositionScratch = new Matrix4f(); + private final Matrix4f modelViewScratch = new Matrix4f(); + private Renderer3D renderer; + private Renderer3D depthRenderer; + private Renderer3D postRenderer; + private Renderer3D postDepthRenderer; + + interface Host extends GameRendererProjectionHelper.BobInvoker { + Minecraft lovisual$minecraft(); + + Camera lovisual$mainCamera(); + + GameRendererProjectionHelper lovisual$projection(); + + float lovisual$spinningTime(); + + float lovisual$spinningSpeed(); + } + + private record Frame(float tickDelta, float fov, Matrix4f position, Matrix4f capturedProjection, + boolean useCapturedMatrices) {} + private record PhasePlan(RenderPhase phase, WorldPhase worldPhase, LoVisualRenderStage stage, + String moduleScope, String section, String flushScope, String flushSection, + boolean setupFrameState) {} + + private static final PhasePlan BEFORE_TRANSLUCENT = new PhasePlan( + RenderPhase.WORLD_BEFORE_TRANSLUCENT, WorldPhase.BEFORE_TRANSLUCENT, + LoVisualRenderStage.WORLD_BEFORE_TRANSLUCENT, "3d:modules_before_translucent", + "modules_before_translucent", "3d:flush_before_translucent", "flush_before_translucent", true); + private static final PhasePlan END_MAIN = new PhasePlan( + RenderPhase.WORLD_AFTER_TRANSLUCENT, WorldPhase.END_MAIN, + LoVisualRenderStage.WORLD_END_MAIN, "3d:modules_end_main", + "modules_end_main", "3d:flush_end_main", "flush_end_main", false); + private static final PhasePlan AFTER_POST_PROCESS = new PhasePlan( + RenderPhase.WORLD_POST_PRE_HAND, WorldPhase.AFTER_POST_PROCESS, + LoVisualRenderStage.WORLD_POST_PROCESS, "3d:post_modules", + "modules", "3d:post_flush", "flush", true); + + void renderWorld(Host host, DeltaTracker tickCounter) { + try (ProfilerPhase.Scope phaseScope = ProfilerPhase.scope("3d:world"); + TracyGpuProfiler.Scope gpuScope = TracyGpuProfiler.beginZone("3d:world")) { + Frame frame = prepareFrame(host, tickCounter); + RenderState.cameraLook = Vec3.directionFromRotation(RenderState.cameraPitch, RenderState.cameraYaw).normalize(); + RenderState.cameraSubmersion = host.lovisual$mainCamera().getFluidInCamera(); + RenderState.worldTranslucent = host.lovisual$projection().isWorldTranslucent(RenderState.cameraSubmersion); + RenderState.frustum = host.lovisual$mainCamera().getCapturedFrustum(); + if (debug.clipSpaceIfNeeded()) return; + + if (renderer == null) { + renderer = new Renderer3D(LoVisualRenderPipelines.WORLD_COLORED_LINES, LoVisualRenderPipelines.WORLD_COLORED); + } + if (depthRenderer == null) { + depthRenderer = new Renderer3D(LoVisualRenderPipelines.WORLD_COLORED_LINES_DEPTH, LoVisualRenderPipelines.WORLD_COLORED_DEPTH); + } + Matrix4fStack mv = RenderSystem.getModelViewStack(); + mv.pushMatrix(); + if (IrisLoVisualFrameHooks.isRenderingAfterIrisFinalization()) { + mv.identity(); + } + mv.mul(frame.position()); + matrices.pushPose(); + Matrix4f projection = buildProjection(host, frame, matrices); + startFrame(tickCounter, frame, projection, mv); + renderPhase(BEFORE_TRANSLUCENT, host, frame, matrices, projection, mv, renderer, depthRenderer, + () -> debug.primitiveIfNeeded(renderer, depthRenderer, host.lovisual$mainCamera(), projection)); + // Start a new batch for END_MAIN so overlays render above grass. + renderer.begin(); + depthRenderer.begin(); + renderPhase(END_MAIN, host, frame, matrices, projection, mv, renderer, depthRenderer, null); + matrices.popPose(); + mv.popMatrix(); + RenderState.rendering3D = false; + } + } + + void renderPostWorld(Host host, DeltaTracker tickCounter) { + try (ProfilerPhase.Scope phaseScope = ProfilerPhase.scope("3d:world_post"); + TracyGpuProfiler.Scope gpuScope = TracyGpuProfiler.beginZone("3d:world_post")) { + Frame frame = prepareFrame(host, tickCounter); + if (postRenderer == null) { + postRenderer = new Renderer3D(LoVisualRenderPipelines.WORLD_COLORED_LINES, LoVisualRenderPipelines.WORLD_COLORED); + } + if (postDepthRenderer == null) { + postDepthRenderer = new Renderer3D(LoVisualRenderPipelines.WORLD_COLORED_LINES_DEPTH, LoVisualRenderPipelines.WORLD_COLORED_DEPTH); + } + Matrix4fStack mv = RenderSystem.getModelViewStack(); + mv.pushMatrix(); + if (IrisLoVisualFrameHooks.isRenderingAfterIrisFinalization()) { + mv.identity(); + } + mv.mul(frame.position()); + postMatrices.pushPose(); + Matrix4f projection = buildProjection(host, frame, postMatrices); + startFrame(tickCounter, frame, projection, mv); + renderPhase(AFTER_POST_PROCESS, host, frame, postMatrices, projection, mv, postRenderer, postDepthRenderer, null); + postMatrices.popPose(); + mv.popMatrix(); + RenderState.rendering3D = false; + } + } + + private Frame prepareFrame(Host host, DeltaTracker tickCounter) { + Camera camera = host.lovisual$mainCamera(); + float tickDelta = tickCounter.getGameTimeDeltaPartialTick(true); + Matrix4f capturedPosition = LoVisualWorldMatrices.positionMatrix(); + Matrix4f capturedProjection = LoVisualWorldMatrices.renderProjectionMatrix(); + boolean useCaptured = capturedPosition != null && capturedProjection != null; + Matrix4f position = useCaptured + ? capturedPosition + : cameraPositionScratch.identity().rotation(camera.rotation().conjugate(host.lovisual$projection().conjugateScratch())); + RenderState.rendering3D = true; + RenderState.tickDelta = tickDelta; + Vec3 capturedCameraPos = LoVisualWorldMatrices.cameraPosition(); + RenderState.cameraPos = capturedCameraPos != null ? capturedCameraPos : camera.position(); + RenderState.cameraRotation.set(camera.rotation()); + RenderState.cameraYaw = camera.yRot(); + RenderState.cameraPitch = camera.xRot(); + return new Frame(tickDelta, camera.getFov(), position, capturedProjection, useCaptured); + } + + private Matrix4f buildProjection(Host host, Frame frame, PoseStack stack) { + if (frame.useCapturedMatrices()) return frame.capturedProjection(); + return host.lovisual$projection().buildWorldProjection(host.lovisual$mainCamera(), frame.fov(), stack, + frame.tickDelta(), host.lovisual$spinningTime(), host.lovisual$spinningSpeed(), host.lovisual$minecraft(), host); + } + + private void startFrame(DeltaTracker tickCounter, Frame frame, Matrix4f projection, Matrix4fStack mv) { + MeshRenderer.setProjection(projection); + LoVisualRenderSystem.beginFrame(frame.tickDelta(), tickCounter.getGameTimeDeltaTicks(), + tickCounter.getRealtimeDeltaTicks(), projection, modelViewScratch.set(mv)); + } + + private void renderPhase(PhasePlan plan, Host host, Frame frame, PoseStack stack, Matrix4f projection, + Matrix4fStack mv, Renderer3D renderer3d, Renderer3D depthRenderer3d, + Runnable beforeModules) { + try (RenderPhaseScope phaseScope = LoVisualRenderSystem.phase(plan.phase())) { + if (plan.setupFrameState()) { + RenderState.worldProjection.set(projection); + RenderState.frustum = new Frustum(frame.position(), projection); + RenderState.frustum.prepare(RenderState.cameraPos.x, RenderState.cameraPos.y, RenderState.cameraPos.z); + if (!frame.useCapturedMatrices()) { + mv.mul(host.lovisual$projection().inverseBobScratch().set(stack.last().pose()).invert()); + } + renderer3d.begin(); + depthRenderer3d.begin(); + } + if (beforeModules != null) beforeModules.run(); + try (ProfilerPhase.Scope scope = ProfilerPhase.scope(plan.moduleScope()); + RenderProfiler3D.Section ignored = RenderProfiler3D.section(plan.section())) { + ModuleManager.renderWorldEngine(plan.worldPhase(), renderer3d, depthRenderer3d, frame.tickDelta()); + AddonRenderPipelineManager.render3D(plan.stage(), plan.worldPhase(), renderer3d, depthRenderer3d, + stack, frame.tickDelta()); + } + try (ProfilerPhase.Scope flushScope = ProfilerPhase.scope(plan.flushScope()); + TracyGpuProfiler.Scope gpuFlush = TracyGpuProfiler.beginZone(plan.flushScope()); + RenderProfiler3D.Section ignored = RenderProfiler3D.section(plan.flushSection())) { + renderer3d.render(stack); + depthRenderer3d.render(stack); + } + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/main/handlers/GameRendererWorldPost.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/main/handlers/GameRendererWorldPost.java new file mode 100644 index 0000000..d3f5c7f --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/main/handlers/GameRendererWorldPost.java @@ -0,0 +1,146 @@ +package dev.loki.lovisual.mixins.render.main; + +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.Minecraft; +import dev.loki.lovisual.addon.pipeline.AddonRenderPipelineManager; +import dev.loki.lovisual.api.v0.render.LoVisualRenderStage; +import dev.loki.lovisual.config.i18n.MainConfig; +import dev.loki.lovisual.render.engine.core.frame.RenderPhase; +import dev.loki.lovisual.render.engine.core.frame.RenderPhaseScope; +import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; +import dev.loki.lovisual.render.engine.depth.WorldSceneDepth; +import dev.loki.lovisual.render.engine.msaa.MsaaWorldTarget; +import dev.loki.lovisual.render.engine.postprocess.post.PostProcessManager; +import dev.loki.lovisual.render.engine.postprocess.post.PostProcessPass; +import dev.loki.lovisual.render.engine.postprocess.render.MenuBackgroundRenderer; +import dev.loki.lovisual.render.engine.profiler.bridge.TracyGpuProfiler; +import dev.loki.lovisual.render.engine.profiler.config.ProfilerPhase; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.renderer.ui.deferred.sched.HudDeferredGuiElement; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.render.engine.visuals.LoVisualVisuals; +import dev.loki.lovisual.render.iris.compat.IrisCompatibilityGuards; +import dev.loki.lovisual.render.iris.runtime.IrisRuntime; +import dev.loki.lovisual.util.screen.ClientScreen; + +/** + * Pre/post-hand post-processing and HUD drain bodies extracted from GameRendererMixin. + */ +final class GameRendererWorldPost { + + private final GameRendererWorldEngine engine; + + GameRendererWorldPost(GameRendererWorldEngine engine) { + this.engine = engine; + } + + void preHand(GameRendererWorldEngine.Host host, DeltaTracker tickCounter) { + if (IrisRuntime.isShaderpackRendererActive() + && IrisCompatibilityGuards.deferIrisFinalizationForSecondHandScene()) { + return; + } + ProfilerPhase.begin("3d:post_pre_hand"); + try (TracyGpuProfiler.Scope gpuScope = TracyGpuProfiler.beginZone("3d:post_pre_hand")) { + Minecraft minecraft = host.lovisual$minecraft(); + if (minecraft != null) { + boolean needsResolvedDepth = !IrisRuntime.isShaderpackRendererActive() + && host.lovisual$projection().needsResolvedMainDepth(); + boolean capturedMsaaDepth = needsResolvedDepth + && MsaaWorldTarget.isActive() + && WorldSceneDepth.captureResolvedMain(MsaaWorldTarget.getMsaaFramebuffer()); + MsaaWorldTarget.resolveToMain(minecraft); + if (needsResolvedDepth && !capturedMsaaDepth) { + WorldSceneDepth.captureResolvedMain(minecraft.gameRenderer.mainRenderTarget()); + } + LoVisualVisuals.renderWorldBase(minecraft, tickCounter.getGameTimeDeltaPartialTick(true)); + } + try (RenderPhaseScope phase = LoVisualRenderSystem.phase(RenderPhase.WORLD_POST_PRE_HAND)) { + PostProcessManager.renderAll(PostProcessPass.Phase.PRE_HAND, + tickCounter.getGameTimeDeltaPartialTick(true)); + renderWorldAfterPreHand(host, tickCounter); + } + } + ProfilerPhase.end("3d:post_pre_hand"); + } + + private void renderWorldAfterPreHand(GameRendererWorldEngine.Host host, DeltaTracker tickCounter) { + Minecraft minecraft = host.lovisual$minecraft(); + if (minecraft == null) { + engine.renderWorld(host, tickCounter); + engine.renderPostWorld(host, tickCounter); + return; + } + + MsaaWorldTarget.begin(minecraft, MainConfig.get().getMsaa3dSamples(), false, true); + try { + engine.renderWorld(host, tickCounter); + engine.renderPostWorld(host, tickCounter); + } finally { + MsaaWorldTarget.resolveToMain(minecraft); + } + } + + void afterHand(GameRendererWorldEngine.Host host, DeltaTracker tickCounter) { + ProfilerPhase.begin("3d:post_after_hand"); + try (TracyGpuProfiler.Scope gpuScope = TracyGpuProfiler.beginZone("3d:post_after_hand")) { + Minecraft minecraft = host.lovisual$minecraft(); + if (minecraft != null) { + MsaaWorldTarget.resolveToMain(minecraft); + } + try (RenderPhaseScope phase = LoVisualRenderSystem.phase(RenderPhase.WORLD_POST_HAND)) { + PostProcessManager.renderAll(PostProcessPass.Phase.POST_HAND, + tickCounter.getGameTimeDeltaPartialTick(true)); + } + } + ProfilerPhase.end("3d:post_after_hand"); + } + + void beforeVanillaGui(Minecraft minecraft, DeltaTracker tickCounter) { + MenuBackgroundRenderer.drainDeferred(minecraft); + Renderer2D.captureWorldGlassSource(); + if (AddonRenderPipelineManager.hasActiveCallbacks(LoVisualRenderStage.SCREEN_BEFORE_VANILLA_GUI)) { + float tickDelta = tickCounter.getGameTimeDeltaPartialTick(true); + Renderer2D.COLOR.begin(); + AddonRenderPipelineManager.render2D(LoVisualRenderStage.SCREEN_BEFORE_VANILLA_GUI, + null, Renderer2D.COLOR, TextRenderer.get(), null, tickDelta); + Renderer2D.COLOR.render(); + } + Renderer2D.drainDeferred2D(Renderer2D.Deferred2DLayer.BEFORE_VANILLA_GUI); + drainHudLayersBelowScreenGui(); + } + + void afterVanillaGui(DeltaTracker tickCounter) { + Renderer2D.drainDeferred2D(Renderer2D.Deferred2DLayer.AFTER_VANILLA_GUI); + if (AddonRenderPipelineManager.hasActiveCallbacks(LoVisualRenderStage.SCREEN_AFTER_VANILLA_GUI)) { + float tickDelta = tickCounter.getGameTimeDeltaPartialTick(true); + Renderer2D.COLOR.begin(); + AddonRenderPipelineManager.render2D(LoVisualRenderStage.SCREEN_AFTER_VANILLA_GUI, + null, Renderer2D.COLOR, TextRenderer.get(), null, tickDelta); + Renderer2D.COLOR.render(); + } + } + + /** + * Gui.extractRenderState() extracts vanilla HUD first and the current Screen after it. + * Normal gameplay keeps LoVisual HUD spliced into that HUD order through HudDeferredGuiElement + * markers. When a real Screen is open, however, the screen must remain the top GUI owner: + * HUD/world overlays are drained before GuiRenderer.render(), + * preserving their internal order while keeping container/inventory GUI above them. + * Explicit SCREEN_TOP/AFTER_VANILLA_GUI overlays (Dynamic Island, ClickGui top layer, etc.) are + * intentionally not drained here. + */ + private static void drainHudLayersBelowScreenGui() { + if (ClientScreen.current() == null) { + return; + } + + Renderer2D.drainDeferred2D(Renderer2D.Deferred2DLayer.HUD_FIRST); + Renderer2D.drainDeferred2D(Renderer2D.Deferred2DLayer.HUD_BEFORE_MISC_OVERLAYS); + Renderer2D.drainDeferred2D(Renderer2D.Deferred2DLayer.HUD_AFTER_MISC_OVERLAYS); + Renderer2D.drainDeferred2D(Renderer2D.Deferred2DLayer.HUD_AFTER_BOSS_BAR); + Renderer2D.drainDeferred2D(Renderer2D.Deferred2DLayer.HUD_BEFORE_DEMO_TIMER); + Renderer2D.drainDeferred2D(Renderer2D.Deferred2DLayer.HUD_BEFORE_CHAT); + Renderer2D.drainDeferred2D(Renderer2D.Deferred2DLayer.HUD_AFTER_SUBTITLES); + Renderer2D.drainDeferred2D(Renderer2D.Deferred2DLayer.HUD_LAST); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/sound/LibraryMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/sound/LibraryMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/sound/LibraryMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/render/sound/LibraryMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/sound/SoundEngineMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/sound/SoundEngineMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/sound/SoundEngineMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/render/sound/SoundEngineMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/text/FontManagerMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/text/FontManagerMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/text/FontManagerMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/render/text/FontManagerMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/security/net/ClientLoginNetworkHandlerMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/security/net/ClientLoginNetworkHandlerMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/security/net/ClientLoginNetworkHandlerMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/security/net/ClientLoginNetworkHandlerMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/security/net/GameOptionsMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/security/net/GameOptionsMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/security/net/GameOptionsMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/security/net/GameOptionsMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/security/net/NetworkUtilsMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/security/net/NetworkUtilsMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/security/net/NetworkUtilsMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/security/net/NetworkUtilsMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/security/text/AbstractSignEditScreenMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/security/text/AbstractSignEditScreenMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/security/text/AbstractSignEditScreenMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/security/text/AbstractSignEditScreenMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/security/text/AnvilScreenMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/security/text/AnvilScreenMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/security/text/AnvilScreenMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/security/text/AnvilScreenMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/security/text/TranslatableTextContentMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/security/text/TranslatableTextContentMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/security/text/TranslatableTextContentMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/security/text/TranslatableTextContentMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/security/text/TranslationStorageMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/security/text/TranslationStorageMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/security/text/TranslationStorageMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/security/text/TranslationStorageMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/sodium/terrain/SodiumBlockRendererMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/sodium/terrain/SodiumBlockRendererMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/sodium/terrain/SodiumBlockRendererMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/sodium/terrain/SodiumBlockRendererMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/sodium/terrain/SodiumChunkMeshFormatsMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/sodium/terrain/SodiumChunkMeshFormatsMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/sodium/terrain/SodiumChunkMeshFormatsMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/sodium/terrain/SodiumChunkMeshFormatsMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/sodium/terrain/SodiumShaderChunkRendererMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/sodium/terrain/SodiumShaderChunkRendererMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/sodium/terrain/SodiumShaderChunkRendererMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/sodium/terrain/SodiumShaderChunkRendererMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/sodium/vertex/SodiumChunkVertexMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/sodium/vertex/SodiumChunkVertexMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/sodium/vertex/SodiumChunkVertexMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/sodium/vertex/SodiumChunkVertexMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/sodium/vertex/SodiumVertexConsumerTrackerMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/sodium/vertex/SodiumVertexConsumerTrackerMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/sodium/vertex/SodiumVertexConsumerTrackerMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/sodium/vertex/SodiumVertexConsumerTrackerMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/sodium/vertex/SodiumWorldRendererFreecamMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/sodium/vertex/SodiumWorldRendererFreecamMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/sodium/vertex/SodiumWorldRendererFreecamMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/sodium/vertex/SodiumWorldRendererFreecamMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/world/block/BlockCollisionSpliteratorMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/world/block/BlockCollisionSpliteratorMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/world/block/BlockCollisionSpliteratorMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/world/block/BlockCollisionSpliteratorMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/world/block/BlockMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/world/block/BlockMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/world/block/BlockMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/world/block/BlockMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/world/block/HoneyBlockMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/world/block/HoneyBlockMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/world/block/HoneyBlockMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/world/block/HoneyBlockMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/world/block/SlimeBlockMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/world/block/SlimeBlockMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/world/block/SlimeBlockMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/world/block/SlimeBlockMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/world/fluid/FlowingFluidMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/world/fluid/FlowingFluidMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/world/fluid/FlowingFluidMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/world/fluid/FlowingFluidMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/world/level/ClientClockManagerMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/world/level/ClientClockManagerMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/world/level/ClientClockManagerMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/world/level/ClientClockManagerMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/world/level/ClientLevelMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/world/level/ClientLevelMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/world/level/ClientLevelMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/world/level/ClientLevelMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/world/level/LevelExtractorMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/world/level/LevelExtractorMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/world/level/LevelExtractorMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/world/level/LevelExtractorMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/world/level/LevelMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/world/level/LevelMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/world/level/LevelMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/world/level/LevelMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/xaero/minimap/AbstractWaypointRenderProviderMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/xaero/minimap/AbstractWaypointRenderProviderMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/xaero/minimap/AbstractWaypointRenderProviderMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/xaero/minimap/AbstractWaypointRenderProviderMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/xaero/worldmap/WaypointRenderProviderMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/xaero/worldmap/WaypointRenderProviderMixin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/xaero/worldmap/WaypointRenderProviderMixin.java rename to mod/src/main/java/dev/loki/lovisual/mixins/xaero/worldmap/WaypointRenderProviderMixin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/LoVisualEntityRenderTypes.java b/mod/src/main/java/dev/loki/lovisual/render/LoVisualEntityRenderTypes.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/LoVisualEntityRenderTypes.java rename to mod/src/main/java/dev/loki/lovisual/render/LoVisualEntityRenderTypes.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/ShaderEspRenderContext.java b/mod/src/main/java/dev/loki/lovisual/render/ShaderEspRenderContext.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/ShaderEspRenderContext.java rename to mod/src/main/java/dev/loki/lovisual/render/ShaderEspRenderContext.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/compat/EntityCullingCompat.java b/mod/src/main/java/dev/loki/lovisual/render/compat/EntityCullingCompat.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/compat/EntityCullingCompat.java rename to mod/src/main/java/dev/loki/lovisual/render/compat/EntityCullingCompat.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/effects/NetherPortalRiftPass.java b/mod/src/main/java/dev/loki/lovisual/render/effects/NetherPortalRiftPass.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/effects/NetherPortalRiftPass.java rename to mod/src/main/java/dev/loki/lovisual/render/effects/NetherPortalRiftPass.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/effects/SleepOverlayPass.java b/mod/src/main/java/dev/loki/lovisual/render/effects/SleepOverlayPass.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/effects/SleepOverlayPass.java rename to mod/src/main/java/dev/loki/lovisual/render/effects/SleepOverlayPass.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/LoVisualRenderEngineBootstrap.java b/mod/src/main/java/dev/loki/lovisual/render/engine/LoVisualRenderEngineBootstrap.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/LoVisualRenderEngineBootstrap.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/LoVisualRenderEngineBootstrap.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/Projection2D.java b/mod/src/main/java/dev/loki/lovisual/render/engine/Projection2D.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/Projection2D.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/Projection2D.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/RenderState.java b/mod/src/main/java/dev/loki/lovisual/render/engine/RenderState.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/RenderState.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/RenderState.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/animation/AnimatedClockText.java b/mod/src/main/java/dev/loki/lovisual/render/engine/animation/AnimatedClockText.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/animation/AnimatedClockText.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/animation/AnimatedClockText.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/animation/AnimatedColorModeOptions.java b/mod/src/main/java/dev/loki/lovisual/render/engine/animation/AnimatedColorModeOptions.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/animation/AnimatedColorModeOptions.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/animation/AnimatedColorModeOptions.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/animation/AnimatedRenderColors.java b/mod/src/main/java/dev/loki/lovisual/render/engine/animation/AnimatedRenderColors.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/animation/AnimatedRenderColors.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/animation/AnimatedRenderColors.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/animation/AnimationUtility.java b/mod/src/main/java/dev/loki/lovisual/render/engine/animation/AnimationUtility.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/animation/AnimationUtility.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/animation/AnimationUtility.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/animation/text/AnimatedGlyphSlot.java b/mod/src/main/java/dev/loki/lovisual/render/engine/animation/text/AnimatedGlyphSlot.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/animation/text/AnimatedGlyphSlot.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/animation/text/AnimatedGlyphSlot.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/animation/text/AnimatedTextLine.java b/mod/src/main/java/dev/loki/lovisual/render/engine/animation/text/AnimatedTextLine.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/animation/text/AnimatedTextLine.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/animation/text/AnimatedTextLine.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/animation/text/AnimatedTextStyle.java b/mod/src/main/java/dev/loki/lovisual/render/engine/animation/text/AnimatedTextStyle.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/animation/text/AnimatedTextStyle.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/animation/text/AnimatedTextStyle.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/animation/text/AnimatedTextTransition.java b/mod/src/main/java/dev/loki/lovisual/render/engine/animation/text/AnimatedTextTransition.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/animation/text/AnimatedTextTransition.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/animation/text/AnimatedTextTransition.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/color/ColorBlendMode.java b/mod/src/main/java/dev/loki/lovisual/render/engine/color/ColorBlendMode.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/color/ColorBlendMode.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/color/ColorBlendMode.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/color/ColorEffects.java b/mod/src/main/java/dev/loki/lovisual/render/engine/color/ColorEffects.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/color/ColorEffects.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/color/ColorEffects.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/color/ColorUtils.java b/mod/src/main/java/dev/loki/lovisual/render/engine/color/ColorUtils.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/color/ColorUtils.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/color/ColorUtils.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/color/RenderColor.java b/mod/src/main/java/dev/loki/lovisual/render/engine/color/RenderColor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/color/RenderColor.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/color/RenderColor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/command/core/UiCommand.java b/mod/src/main/java/dev/loki/lovisual/render/engine/command/core/UiCommand.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/command/core/UiCommand.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/command/core/UiCommand.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/command/core/UiCommandBuffer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/command/core/UiCommandBuffer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/command/core/UiCommandBuffer.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/command/core/UiCommandBuffer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/command/core/UiCommandKind.java b/mod/src/main/java/dev/loki/lovisual/render/engine/command/core/UiCommandKind.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/command/core/UiCommandKind.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/command/core/UiCommandKind.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/command/core/UiCommandStats.java b/mod/src/main/java/dev/loki/lovisual/render/engine/command/core/UiCommandStats.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/command/core/UiCommandStats.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/command/core/UiCommandStats.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/command/effect/UiBatchBackendCommand.java b/mod/src/main/java/dev/loki/lovisual/render/engine/command/effect/UiBatchBackendCommand.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/command/effect/UiBatchBackendCommand.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/command/effect/UiBatchBackendCommand.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/command/effect/UiEffectCommand.java b/mod/src/main/java/dev/loki/lovisual/render/engine/command/effect/UiEffectCommand.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/command/effect/UiEffectCommand.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/command/effect/UiEffectCommand.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/command/effect/UiEffectRegionCommand.java b/mod/src/main/java/dev/loki/lovisual/render/engine/command/effect/UiEffectRegionCommand.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/command/effect/UiEffectRegionCommand.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/command/effect/UiEffectRegionCommand.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/command/shape/UiPathCommand.java b/mod/src/main/java/dev/loki/lovisual/render/engine/command/shape/UiPathCommand.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/command/shape/UiPathCommand.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/command/shape/UiPathCommand.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/command/shape/UiPrimitiveCommand.java b/mod/src/main/java/dev/loki/lovisual/render/engine/command/shape/UiPrimitiveCommand.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/command/shape/UiPrimitiveCommand.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/command/shape/UiPrimitiveCommand.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/command/shape/UiShapeCommand.java b/mod/src/main/java/dev/loki/lovisual/render/engine/command/shape/UiShapeCommand.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/command/shape/UiShapeCommand.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/command/shape/UiShapeCommand.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/command/texture/UiItemCommand.java b/mod/src/main/java/dev/loki/lovisual/render/engine/command/texture/UiItemCommand.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/command/texture/UiItemCommand.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/command/texture/UiItemCommand.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/command/texture/UiTextCommand.java b/mod/src/main/java/dev/loki/lovisual/render/engine/command/texture/UiTextCommand.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/command/texture/UiTextCommand.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/command/texture/UiTextCommand.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/command/texture/UiTextureCommand.java b/mod/src/main/java/dev/loki/lovisual/render/engine/command/texture/UiTextureCommand.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/command/texture/UiTextureCommand.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/command/texture/UiTextureCommand.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/command/texture/UiTextureDrawCommand.java b/mod/src/main/java/dev/loki/lovisual/render/engine/command/texture/UiTextureDrawCommand.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/command/texture/UiTextureDrawCommand.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/command/texture/UiTextureDrawCommand.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/command/world/RenderCommandBuffer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/command/world/RenderCommandBuffer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/command/world/RenderCommandBuffer.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/command/world/RenderCommandBuffer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/command/world/UiStatsSnapshot.java b/mod/src/main/java/dev/loki/lovisual/render/engine/command/world/UiStatsSnapshot.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/command/world/UiStatsSnapshot.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/command/world/UiStatsSnapshot.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/command/world/WorldCommandBuffer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/command/world/WorldCommandBuffer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/command/world/WorldCommandBuffer.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/command/world/WorldCommandBuffer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/compat/immediatelyfast/ImmediatelyFastBridge.java b/mod/src/main/java/dev/loki/lovisual/render/engine/compat/immediatelyfast/ImmediatelyFastBridge.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/compat/immediatelyfast/ImmediatelyFastBridge.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/compat/immediatelyfast/ImmediatelyFastBridge.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/compat/immediatelyfast/ImmediatelyFastRuntime.java b/mod/src/main/java/dev/loki/lovisual/render/engine/compat/immediatelyfast/ImmediatelyFastRuntime.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/compat/immediatelyfast/ImmediatelyFastRuntime.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/compat/immediatelyfast/ImmediatelyFastRuntime.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/compat/immediatelyfast/ImmediatelyFastRuntimeSnapshot.java b/mod/src/main/java/dev/loki/lovisual/render/engine/compat/immediatelyfast/ImmediatelyFastRuntimeSnapshot.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/compat/immediatelyfast/ImmediatelyFastRuntimeSnapshot.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/compat/immediatelyfast/ImmediatelyFastRuntimeSnapshot.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/core/context/CameraContext.java b/mod/src/main/java/dev/loki/lovisual/render/engine/core/context/CameraContext.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/core/context/CameraContext.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/core/context/CameraContext.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/core/context/FramebufferContext.java b/mod/src/main/java/dev/loki/lovisual/render/engine/core/context/FramebufferContext.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/core/context/FramebufferContext.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/core/context/FramebufferContext.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/core/context/ViewportContext.java b/mod/src/main/java/dev/loki/lovisual/render/engine/core/context/ViewportContext.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/core/context/ViewportContext.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/core/context/ViewportContext.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/core/frame/FrameLifecycle.java b/mod/src/main/java/dev/loki/lovisual/render/engine/core/frame/FrameLifecycle.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/core/frame/FrameLifecycle.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/core/frame/FrameLifecycle.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/core/frame/RenderFrameContext.java b/mod/src/main/java/dev/loki/lovisual/render/engine/core/frame/RenderFrameContext.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/core/frame/RenderFrameContext.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/core/frame/RenderFrameContext.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/core/frame/RenderPhase.java b/mod/src/main/java/dev/loki/lovisual/render/engine/core/frame/RenderPhase.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/core/frame/RenderPhase.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/core/frame/RenderPhase.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/core/frame/RenderPhaseScope.java b/mod/src/main/java/dev/loki/lovisual/render/engine/core/frame/RenderPhaseScope.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/core/frame/RenderPhaseScope.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/core/frame/RenderPhaseScope.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/core/policy/DepthProvider.java b/mod/src/main/java/dev/loki/lovisual/render/engine/core/policy/DepthProvider.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/core/policy/DepthProvider.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/core/policy/DepthProvider.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/core/policy/LightPolicy.java b/mod/src/main/java/dev/loki/lovisual/render/engine/core/policy/LightPolicy.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/core/policy/LightPolicy.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/core/policy/LightPolicy.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/core/policy/VisibilityProvider.java b/mod/src/main/java/dev/loki/lovisual/render/engine/core/policy/VisibilityProvider.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/core/policy/VisibilityProvider.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/core/policy/VisibilityProvider.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/core/policy/VisibilityQuery.java b/mod/src/main/java/dev/loki/lovisual/render/engine/core/policy/VisibilityQuery.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/core/policy/VisibilityQuery.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/core/policy/VisibilityQuery.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/core/policy/fog/FogProvider.java b/mod/src/main/java/dev/loki/lovisual/render/engine/core/policy/fog/FogProvider.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/core/policy/fog/FogProvider.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/core/policy/fog/FogProvider.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/core/policy/fog/VanillaWorldFogProvider.java b/mod/src/main/java/dev/loki/lovisual/render/engine/core/policy/fog/VanillaWorldFogProvider.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/core/policy/fog/VanillaWorldFogProvider.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/core/policy/fog/VanillaWorldFogProvider.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/core/system/LoVisualRenderSystem.java b/mod/src/main/java/dev/loki/lovisual/render/engine/core/system/LoVisualRenderSystem.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/core/system/LoVisualRenderSystem.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/core/system/LoVisualRenderSystem.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/core/system/LoVisualWorldMatrices.java b/mod/src/main/java/dev/loki/lovisual/render/engine/core/system/LoVisualWorldMatrices.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/core/system/LoVisualWorldMatrices.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/core/system/LoVisualWorldMatrices.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/debug/RenderThread2DDebugRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/debug/RenderThread2DDebugRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/debug/RenderThread2DDebugRenderer.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/debug/RenderThread2DDebugRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/depth/PreTranslucentDepth.java b/mod/src/main/java/dev/loki/lovisual/render/engine/depth/PreTranslucentDepth.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/depth/PreTranslucentDepth.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/depth/PreTranslucentDepth.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/depth/WorldSceneDepth.java b/mod/src/main/java/dev/loki/lovisual/render/engine/depth/WorldSceneDepth.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/depth/WorldSceneDepth.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/depth/WorldSceneDepth.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/framegraph/LoVisualFrameGraph.java b/mod/src/main/java/dev/loki/lovisual/render/engine/framegraph/LoVisualFrameGraph.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/framegraph/LoVisualFrameGraph.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/framegraph/LoVisualFrameGraph.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/framegraph/PassScheduler.java b/mod/src/main/java/dev/loki/lovisual/render/engine/framegraph/PassScheduler.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/framegraph/PassScheduler.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/framegraph/PassScheduler.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/framegraph/RenderPassNode.java b/mod/src/main/java/dev/loki/lovisual/render/engine/framegraph/RenderPassNode.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/framegraph/RenderPassNode.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/framegraph/RenderPassNode.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/guard/LegacyRenderPath.java b/mod/src/main/java/dev/loki/lovisual/render/engine/guard/LegacyRenderPath.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/guard/LegacyRenderPath.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/guard/LegacyRenderPath.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/guard/RenderArchitectureGuard.java b/mod/src/main/java/dev/loki/lovisual/render/engine/guard/RenderArchitectureGuard.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/guard/RenderArchitectureGuard.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/guard/RenderArchitectureGuard.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/guard/boundary/RenderBoundary.java b/mod/src/main/java/dev/loki/lovisual/render/engine/guard/boundary/RenderBoundary.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/guard/boundary/RenderBoundary.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/guard/boundary/RenderBoundary.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/guard/boundary/RenderBoundaryAudit.java b/mod/src/main/java/dev/loki/lovisual/render/engine/guard/boundary/RenderBoundaryAudit.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/guard/boundary/RenderBoundaryAudit.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/guard/boundary/RenderBoundaryAudit.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/guard/boundary/RenderBoundaryExempt.java b/mod/src/main/java/dev/loki/lovisual/render/engine/guard/boundary/RenderBoundaryExempt.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/guard/boundary/RenderBoundaryExempt.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/guard/boundary/RenderBoundaryExempt.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/light/LightmapState.java b/mod/src/main/java/dev/loki/lovisual/render/engine/light/LightmapState.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/light/LightmapState.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/light/LightmapState.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/math/ColorMath.java b/mod/src/main/java/dev/loki/lovisual/render/engine/math/ColorMath.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/math/ColorMath.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/math/ColorMath.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/math/HudScale.java b/mod/src/main/java/dev/loki/lovisual/render/engine/math/HudScale.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/math/HudScale.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/math/HudScale.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/math/RenderMath.java b/mod/src/main/java/dev/loki/lovisual/render/engine/math/RenderMath.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/math/RenderMath.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/math/RenderMath.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/msaa/MsaaFramebuffer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/msaa/MsaaFramebuffer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/msaa/MsaaFramebuffer.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/msaa/MsaaFramebuffer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/msaa/MsaaResolveUtil.java b/mod/src/main/java/dev/loki/lovisual/render/engine/msaa/MsaaResolveUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/msaa/MsaaResolveUtil.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/msaa/MsaaResolveUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/msaa/MsaaTextureRegistry.java b/mod/src/main/java/dev/loki/lovisual/render/engine/msaa/MsaaTextureRegistry.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/msaa/MsaaTextureRegistry.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/msaa/MsaaTextureRegistry.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/msaa/MsaaWorldTarget.java b/mod/src/main/java/dev/loki/lovisual/render/engine/msaa/MsaaWorldTarget.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/msaa/MsaaWorldTarget.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/msaa/MsaaWorldTarget.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/DepthTestFunction.java b/mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/DepthTestFunction.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/DepthTestFunction.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/DepthTestFunction.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/ExtendedRenderPipelineBuilder.java b/mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/ExtendedRenderPipelineBuilder.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/ExtendedRenderPipelineBuilder.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/ExtendedRenderPipelineBuilder.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/LoVisualRenderPipelines.java b/mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/LoVisualRenderPipelines.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/LoVisualRenderPipelines.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/LoVisualRenderPipelines.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/PipelineTemplates.java b/mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/PipelineTemplates.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/PipelineTemplates.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/PipelineTemplates.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/ScreenPipelines.java b/mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/ScreenPipelines.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/ScreenPipelines.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/ScreenPipelines.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/UiPipelines.java b/mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/UiPipelines.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/UiPipelines.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/UiPipelines.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/WorldPipelines.java b/mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/WorldPipelines.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/WorldPipelines.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/WorldPipelines.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/blur/DepthOfFieldDepthSource.java b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/blur/DepthOfFieldDepthSource.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/blur/DepthOfFieldDepthSource.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/blur/DepthOfFieldDepthSource.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/blur/DepthOfFieldQuality.java b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/blur/DepthOfFieldQuality.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/blur/DepthOfFieldQuality.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/blur/DepthOfFieldQuality.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/blur/MotionBlurMode.java b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/blur/MotionBlurMode.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/blur/MotionBlurMode.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/blur/MotionBlurMode.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/blur/MotionBlurResources.java b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/blur/MotionBlurResources.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/blur/MotionBlurResources.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/blur/MotionBlurResources.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/LegacyPostProcessGraphPass.java b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/LegacyPostProcessGraphPass.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/LegacyPostProcessGraphPass.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/LegacyPostProcessGraphPass.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/PostProcessGraph.java b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/PostProcessGraph.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/PostProcessGraph.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/PostProcessGraph.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/PostProcessGraphPass.java b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/PostProcessGraphPass.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/PostProcessGraphPass.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/PostProcessGraphPass.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/resource/HistoryBufferManager.java b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/resource/HistoryBufferManager.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/resource/HistoryBufferManager.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/resource/HistoryBufferManager.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/resource/PostProcessGraphResources.java b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/resource/PostProcessGraphResources.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/resource/PostProcessGraphResources.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/resource/PostProcessGraphResources.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/resource/PostProcessResolution.java b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/resource/PostProcessResolution.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/resource/PostProcessResolution.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/resource/PostProcessResolution.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/resource/PostProcessResource.java b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/resource/PostProcessResource.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/resource/PostProcessResource.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/resource/PostProcessResource.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/post/PostProcessContext.java b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/post/PostProcessContext.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/post/PostProcessContext.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/post/PostProcessContext.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/post/PostProcessManager.java b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/post/PostProcessManager.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/post/PostProcessManager.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/post/PostProcessManager.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/post/PostProcessPass.java b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/post/PostProcessPass.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/post/PostProcessPass.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/post/PostProcessPass.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/render/MenuBackgroundRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/render/MenuBackgroundRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/render/MenuBackgroundRenderer.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/render/MenuBackgroundRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/render/SkyShaderRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/render/SkyShaderRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/render/SkyShaderRenderer.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/render/SkyShaderRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/render/SkyShaderStarryRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/render/SkyShaderStarryRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/render/SkyShaderStarryRenderer.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/postprocess/render/SkyShaderStarryRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/prewarm/RenderPrewarmContributors.java b/mod/src/main/java/dev/loki/lovisual/render/engine/prewarm/RenderPrewarmContributors.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/prewarm/RenderPrewarmContributors.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/prewarm/RenderPrewarmContributors.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/prewarm/RenderPrewarmManager.java b/mod/src/main/java/dev/loki/lovisual/render/engine/prewarm/RenderPrewarmManager.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/prewarm/RenderPrewarmManager.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/prewarm/RenderPrewarmManager.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/bridge/DevProfilerBridge.java b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/bridge/DevProfilerBridge.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/bridge/DevProfilerBridge.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/profiler/bridge/DevProfilerBridge.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/bridge/JfrProfiler.java b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/bridge/JfrProfiler.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/bridge/JfrProfiler.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/profiler/bridge/JfrProfiler.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/bridge/TracyGpuProfiler.java b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/bridge/TracyGpuProfiler.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/bridge/TracyGpuProfiler.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/profiler/bridge/TracyGpuProfiler.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/bridge/TracyProfiler.java b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/bridge/TracyProfiler.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/bridge/TracyProfiler.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/profiler/bridge/TracyProfiler.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/config/ProfilerPhase.java b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/config/ProfilerPhase.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/config/ProfilerPhase.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/profiler/config/ProfilerPhase.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/config/ProfilerSettings.java b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/config/ProfilerSettings.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/config/ProfilerSettings.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/profiler/config/ProfilerSettings.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/frame/FrameStutterProfiler.java b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/frame/FrameStutterProfiler.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/frame/FrameStutterProfiler.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/profiler/frame/FrameStutterProfiler.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/frame/GlSyncTracker.java b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/frame/GlSyncTracker.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/frame/GlSyncTracker.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/profiler/frame/GlSyncTracker.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/frame/RenderCostProfiler.java b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/frame/RenderCostProfiler.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/frame/RenderCostProfiler.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/profiler/frame/RenderCostProfiler.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/frame/RenderFrameProfiler.java b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/frame/RenderFrameProfiler.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/frame/RenderFrameProfiler.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/profiler/frame/RenderFrameProfiler.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/render/ProfilerLog.java b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/render/ProfilerLog.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/render/ProfilerLog.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/profiler/render/ProfilerLog.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/render/RenderProfiler2D.java b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/render/RenderProfiler2D.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/render/RenderProfiler2D.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/profiler/render/RenderProfiler2D.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/render/RenderProfiler3D.java b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/render/RenderProfiler3D.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/render/RenderProfiler3D.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/profiler/render/RenderProfiler3D.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/render/SamplingProfiler.java b/mod/src/main/java/dev/loki/lovisual/render/engine/profiler/render/SamplingProfiler.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/render/SamplingProfiler.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/profiler/render/SamplingProfiler.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/FullScreenRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/FullScreenRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/FullScreenRenderer.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/FullScreenRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/MeshRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/MeshRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/MeshRenderer.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/MeshRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/Renderer2D.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/Renderer2D.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/Renderer2D.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/Renderer2D.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/Renderer3D.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/Renderer3D.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/Renderer3D.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/Renderer3D.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/Batch2D.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/Batch2D.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/Batch2D.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/Batch2D.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/Chamfered2D.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/Chamfered2D.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/Chamfered2D.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/Chamfered2D.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/Rects2D.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/Rects2D.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/Rects2D.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/Rects2D.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/Strokes2D.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/Strokes2D.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/Strokes2D.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/Strokes2D.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/fx/Blur2D.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/fx/Blur2D.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/fx/Blur2D.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/fx/Blur2D.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/fx/Effects2D.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/fx/Effects2D.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/fx/Effects2D.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/fx/Effects2D.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/fx/Glass2D.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/fx/Glass2D.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/fx/Glass2D.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/fx/Glass2D.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/fx/Smoke2D.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/fx/Smoke2D.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/fx/Smoke2D.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/fx/Smoke2D.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/media/Items2D.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/media/Items2D.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/media/Items2D.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/media/Items2D.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/media/Textures2D.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/media/Textures2D.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/media/Textures2D.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/media/Textures2D.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/path/Circles2D.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/path/Circles2D.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/path/Circles2D.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/path/Circles2D.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/path/Connectors2D.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/path/Connectors2D.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/path/Connectors2D.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/path/Connectors2D.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/path/Lines2D.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/path/Lines2D.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/path/Lines2D.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/path/Lines2D.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/path/Polygons2D.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/path/Polygons2D.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/path/Polygons2D.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/path/Polygons2D.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/DrawBatch.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/DrawBatch.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/DrawBatch.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/DrawBatch.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ItemBatch.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ItemBatch.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ItemBatch.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ItemBatch.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/TextBatch.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/TextBatch.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/TextBatch.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/TextBatch.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/UiBatcher.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/UiBatcher.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/UiBatcher.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/UiBatcher.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/OrderedUiBatcher.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/OrderedUiBatcher.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/OrderedUiBatcher.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/OrderedUiBatcher.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/UiBatchPlan.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/UiBatchPlan.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/UiBatchPlan.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/UiBatchPlan.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/UiBatchStats.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/UiBatchStats.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/UiBatchStats.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/UiBatchStats.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/UiBatchType.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/UiBatchType.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/UiBatchType.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/UiBatchType.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/LiquidGlassTextFlush.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/LiquidGlassTextFlush.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/LiquidGlassTextFlush.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/LiquidGlassTextFlush.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/OrderedDeferral.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/OrderedDeferral.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/OrderedDeferral.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/OrderedDeferral.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/OrderedDrawBatchFlush.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/OrderedDrawBatchFlush.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/OrderedDrawBatchFlush.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/OrderedDrawBatchFlush.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/OrderedFlushRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/OrderedFlushRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/OrderedFlushRenderer.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/OrderedFlushRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/order/DrawBatchPool.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/order/DrawBatchPool.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/order/DrawBatchPool.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/order/DrawBatchPool.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/order/ItemTextPools.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/order/ItemTextPools.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/order/ItemTextPools.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/order/ItemTextPools.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/order/OrderState.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/order/OrderState.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/order/OrderState.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/order/OrderState.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/BlurSource.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/BlurSource.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/BlurSource.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/BlurSource.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/FrameBlurCacheEntry.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/FrameBlurCacheEntry.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/FrameBlurCacheEntry.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/FrameBlurCacheEntry.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/SharedBlurProfile.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/SharedBlurProfile.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/SharedBlurProfile.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/SharedBlurProfile.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/UiBlurResources.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/UiBlurResources.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/UiBlurResources.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/UiBlurResources.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/kawase/KawaseBlurPreparer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/kawase/KawaseBlurPreparer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/kawase/KawaseBlurPreparer.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/kawase/KawaseBlurPreparer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/kawase/KawasePass.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/kawase/KawasePass.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/kawase/KawasePass.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/kawase/KawasePass.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/kawase/SharedKawaseBlur.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/kawase/SharedKawaseBlur.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/kawase/SharedKawaseBlur.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/kawase/SharedKawaseBlur.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/Deferred2DSubmit.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/Deferred2DSubmit.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/Deferred2DSubmit.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/Deferred2DSubmit.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/DeferredActionSubmit.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/DeferredActionSubmit.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/DeferredActionSubmit.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/DeferredActionSubmit.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/DeferredOrderedSubmit.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/DeferredOrderedSubmit.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/DeferredOrderedSubmit.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/DeferredOrderedSubmit.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/DeferredTexturedSubmit.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/DeferredTexturedSubmit.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/DeferredTexturedSubmit.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/DeferredTexturedSubmit.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/sched/HudDeferredGuiElement.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/sched/HudDeferredGuiElement.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/sched/HudDeferredGuiElement.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/sched/HudDeferredGuiElement.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/sched/UiDeferredScheduler.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/sched/UiDeferredScheduler.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/sched/UiDeferredScheduler.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/deferred/sched/UiDeferredScheduler.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/dispatch/UiDirectTexturedRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/dispatch/UiDirectTexturedRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/dispatch/UiDirectTexturedRenderer.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/dispatch/UiDirectTexturedRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/dispatch/UiRenderDispatcher.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/dispatch/UiRenderDispatcher.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/dispatch/UiRenderDispatcher.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/dispatch/UiRenderDispatcher.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/dispatch/UiRendererSubsystem.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/dispatch/UiRendererSubsystem.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/dispatch/UiRendererSubsystem.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/dispatch/UiRendererSubsystem.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/box/UiBoxPathBuilder.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/box/UiBoxPathBuilder.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/box/UiBoxPathBuilder.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/box/UiBoxPathBuilder.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/box/UiBoxShape.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/box/UiBoxShape.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/box/UiBoxShape.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/box/UiBoxShape.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/box/UiChamferRadii.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/box/UiChamferRadii.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/box/UiChamferRadii.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/box/UiChamferRadii.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/box/UiRect.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/box/UiRect.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/box/UiRect.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/box/UiRect.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/corner/UiCornerKind.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/corner/UiCornerKind.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/corner/UiCornerKind.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/corner/UiCornerKind.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/corner/UiCornerMode.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/corner/UiCornerMode.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/corner/UiCornerMode.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/corner/UiCornerMode.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/corner/UiCornerRadii.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/corner/UiCornerRadii.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/corner/UiCornerRadii.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/corner/UiCornerRadii.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/corner/UiCornerSpec.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/corner/UiCornerSpec.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/corner/UiCornerSpec.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/corner/UiCornerSpec.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/UiEdgeKind.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/UiEdgeKind.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/UiEdgeKind.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/UiEdgeKind.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/UiEdgeSpec.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/UiEdgeSpec.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/UiEdgeSpec.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/UiEdgeSpec.java diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/UiMeshGeometry.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/UiMeshGeometry.java new file mode 100644 index 0000000..7a622ea --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/UiMeshGeometry.java @@ -0,0 +1,99 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ +package dev.loki.lovisual.render.engine.renderer.ui.draw.edge; + +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; + +/** + * Facade for mesh tessellation, gradient and path geometry. + * Delegates to helpers in geometry/ and mesh/ subpackages; keeps same package for compatibility. + */ +public final class UiMeshGeometry { + private UiMeshGeometry() {} + // gradient / color + public static void computeLinearGradientColors(float w,float h,int s,int e,float a,float o,int[] out){UiGradientGeometry.computeLinearGradientColors(w,h,s,e,a,o,out);} + public static float clamp01(float v){return UiGradientGeometry.clamp01(v);} + public static float packLiquidGlassPayload(float d,float s,float b){return UiGradientGeometry.packLiquidGlassPayload(d,s,b);} + public static float normalizeDegrees(float a){return UiGradientGeometry.normalizeDegrees(a);} + public static int mixArgb(int s,int e,float t){return UiGradientGeometry.mixArgb(s,e,t);} + // rect / chamfer + public static float clampRoundedRadius(float r,double w,double h){return UiRectGeometry.clampRoundedRadius(r,w,h);} + public static float clampChamfer(double c,double w,double h){return UiRectGeometry.clampChamfer(c,w,h);} + public static float clampChamferAxis(double c,double l){return UiRectGeometry.clampChamferAxis(c,l);} + public static void normalizeCornerRadii(double w,double h,float tl,float tr,float br,float bl,float[] out){UiRectGeometry.normalizeCornerRadii(w,h,tl,tr,br,bl,out);} + public static void normalizeChamfers(double w,double h,float tl,float tr,float br,float bl,float[] out){UiRectGeometry.normalizeChamfers(w,h,tl,tr,br,bl,out);} + public static int buildNotchedRect(double[] o,double x,double y,double w,double h,double nw,double nd){return UiRectGeometry.buildNotchedRect(o,x,y,w,h,nw,nd);} + // polygon / quad + public static void appendColoredQuad(MeshBuilder m,double x,double y,double w,double h,int tl,int tr,int br,int bl){UiPolygonTessellator.appendColoredQuad(m,x,y,w,h,tl,tr,br,bl);} + public static void appendPolygon(MeshBuilder m,double[] p,int c,int[] idx,int[] v,int argb){UiPolygonTessellator.appendPolygon(m,p,c,idx,v,argb);} + public static void appendPolygonGradient(MeshBuilder m,double[] p,int c,int[] idx,int[] v,double x,double y,double w,double h,int s,int e,float a,float o){UiPolygonTessellator.appendPolygonGradient(m,p,c,idx,v,x,y,w,h,s,e,a,o);} + // polyline / line + public static void appendPolyline(MeshBuilder m,double[] p,int c,double t,boolean cl,int argb,boolean rc){UiPolylineGeometry.appendPolyline(m,p,c,t,cl,argb,rc);} + public static void appendPolylineGradient(MeshBuilder m,double[] p,int c,double t,boolean cl,int s,int e,boolean rc){UiPolylineGeometry.appendPolylineGradient(m,p,c,t,cl,s,e,rc);} + public static void appendPolylineLinearGradient(MeshBuilder m,double[] p,int c,double t,boolean cl,double x,double y,double w,double h,int s,int e,float a,float o,boolean rc){UiPolylineGeometry.appendPolylineLinearGradient(m,p,c,t,cl,x,y,w,h,s,e,a,o,rc);} + // arc / anchor (delegated to gradient helper for circle/arc primitives) + public static void roundedRectAnchor(double[] o,int off,double x,double y,double w,double h,double r,double tx,double ty){UiGradientGeometry.roundedRectAnchor(o,off,x,y,w,h,r,tx,ty);} + public static int buildBezier(double[] o,double x1,double y1,double cx1,double cy1,double cx2,double cy2,double x2,double y2,int s){return UiGradientGeometry.buildBezier(o,x1,y1,cx1,cy1,cx2,cy2,x2,y2,s);} + + // rect shape builders (kept in facade: rect primitive) + public static int buildRoundedProgressRect(double[] out,double[] work,double x,double y,double w,double h,double r,double clipX,boolean fromRight){ + int c=buildRoundedRectPolygon(work,x,y,w,h,r); + if(c<3) return 0; + return clipPolygonVertical(work,c,out,clipX,fromRight); + } + public static int buildRoundedRectPolygon(double[] out,double x,double y,double w,double h,double radius){ + double ww=Math.max(0.0,w); double hh=Math.max(0.0,h); + if(ww<=0.0||hh<=0.0) return 0; + double x2=x+ww; double y2=y+hh; + double r=Math.max(0.0,Math.min(Math.min(ww,hh)*0.5,radius)); + int[] cnt={0}; + if(r<=0.001){add(out,out.length/2,cnt,x,y);add(out,out.length/2,cnt,x2,y);add(out,out.length/2,cnt,x2,y2);add(out,out.length/2,cnt,x,y2);return cnt[0];} + int seg=Math.max(4,Math.min(10,(int)Math.ceil(r*1.35))); + add(out,out.length/2,cnt,x+r,y);add(out,out.length/2,cnt,x2-r,y); + addArc(out,out.length/2,cnt,x2-r,y+r,r,-90.0,0.0,seg); + add(out,out.length/2,cnt,x2,y2-r);addArc(out,out.length/2,cnt,x2-r,y2-r,r,0.0,90.0,seg); + add(out,out.length/2,cnt,x+r,y2);addArc(out,out.length/2,cnt,x+r,y2-r,r,90.0,180.0,seg); + add(out,out.length/2,cnt,x,y+r);addArc(out,out.length/2,cnt,x+r,y+r,r,180.0,270.0,seg); + return removeClosingDuplicate(out,cnt[0]); + } + private static int removeClosingDuplicate(double[] p,int c){ + if(p==null||c<2) return c; + double fx=p[0],fy=p[1],lx=p[(c-1)*2],ly=p[(c-1)*2+1]; + if(Math.abs(fx-lx)<0.0001&&Math.abs(fy-ly)<0.0001) return c-1; + return c; + } + private static void add(double[] o,int max,int[] cnt,double x,double y){ + if(o==null||cnt==null||cnt.length==0) return; + int i=cnt[0]; if(i<0||i>=max) return; + int b=i*2; if(b+1>=o.length) return; + o[b]=x; o[b+1]=y; cnt[0]=i+1; + } + private static void addArc(double[] o,int max,int[] cnt,double cx,double cy,double r,double s,double e,int seg){ + int ss=Math.max(1,seg); + for(int i=1;i<=ss;i++){double t=i/(double)ss; double a=Math.toRadians(s+(e-s)*t); add(o,max,cnt,cx+Math.cos(a)*r,cy+Math.sin(a)*r);} + } + private static int clipPolygonVertical(double[] src,int sc,double[] out,double clipX,boolean keepRight){ + int maxOut=out.length/2; int[] cnt={0}; + if(src==null||out==null||sc<3||maxOut<=0) return 0; + for(int i=0;i=clipX-0.0001:sx<=clipX+0.0001; + boolean eIn=keepRight?ex>=clipX-0.0001:ex<=clipX+0.0001; + if(sIn&&eIn) add(out,maxOut,cnt,ex,ey); + else if(sIn) addVerticalIntersection(out,maxOut,cnt,sx,sy,ex,ey,clipX); + else if(eIn){addVerticalIntersection(out,maxOut,cnt,sx,sy,ex,ey,clipX);add(out,maxOut,cnt,ex,ey);} + } + return cnt[0]; + } + private static void addVerticalIntersection(double[] o,int max,int[] c,double sx,double sy,double ex,double ey,double clipX){ + double dx=ex-sx; + if(Math.abs(dx)<=0.000001){add(o,max,c,clipX,ey);return;} + double t=(clipX-sx)/dx; t=Math.max(0.0,Math.min(1.0,t)); + add(o,max,c,clipX,sy+(ey-sy)*t); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/UiStroke.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/UiStroke.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/UiStroke.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/UiStroke.java diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/geometry/UiGradientGeometry.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/geometry/UiGradientGeometry.java new file mode 100644 index 0000000..c5d7c7a --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/geometry/UiGradientGeometry.java @@ -0,0 +1,135 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ +package dev.loki.lovisual.render.engine.renderer.ui.draw.edge; + +public final class UiGradientGeometry { + private UiGradientGeometry() {} + public static void computeLinearGradientColors(float width, float height, int startArgb, int endArgb, float angleDeg, float offsetPx, int[] out) { + float angle = (float) Math.toRadians(angleDeg); + float dirX = (float) Math.cos(angle); + float dirY = (float) Math.sin(angle); + float p0 = 0.0f; + float p1 = width * dirX; + float p2 = height * dirY; + float p3 = width * dirX + height * dirY; + float minProj = Math.min(Math.min(p0, p1), Math.min(p2, p3)); + float maxProj = Math.max(Math.max(p0, p1), Math.max(p2, p3)); + float range = Math.max(0.0001f, maxProj - minProj); + float t0 = clamp01((p0 + offsetPx - minProj) / range); + float t1 = clamp01((p1 + offsetPx - minProj) / range); + float t2 = clamp01((p3 + offsetPx - minProj) / range); + float t3 = clamp01((p2 + offsetPx - minProj) / range); + out[0] = mixArgb(startArgb, endArgb, t0); + out[1] = mixArgb(startArgb, endArgb, t1); + out[2] = mixArgb(startArgb, endArgb, t2); + out[3] = mixArgb(startArgb, endArgb, t3); + } + static int linearGradientColorAt(double x, double y, double width, double height, int startArgb, int endArgb, float angleDeg, float offsetPx) { + float angle = (float) Math.toRadians(angleDeg); + float dirX = (float) Math.cos(angle); + float dirY = (float) Math.sin(angle); + float w = (float) width; + float h = (float) height; + float p0 = 0.0f; + float p1 = w * dirX; + float p2 = h * dirY; + float p3 = w * dirX + h * dirY; + float minProj = Math.min(Math.min(p0, p1), Math.min(p2, p3)); + float maxProj = Math.max(Math.max(p0, p1), Math.max(p2, p3)); + float range = Math.max(0.0001f, maxProj - minProj); + float p = (float) x * dirX + (float) y * dirY; + return mixArgb(startArgb, endArgb, clamp01((p + offsetPx - minProj) / range)); + } + public static float clamp01(float v) { + if (v < 0.0f) return 0.0f; + if (v > 1.0f) return 1.0f; + return v; + } + public static float packLiquidGlassPayload(float distortPx, float squirclePower, float blurAlpha) { + float distort = Math.max(0.0f, Math.min(0.35f, distortPx)); + float smoothness = squirclePower < 1.5f ? 2.0f : Math.max(2.0f, Math.min(15.0f, Math.round(squirclePower))); + float blurBucket = Math.round(clamp01(blurAlpha) * 100.0f); + return smoothness * 10000.0f + blurBucket + distort; + } + public static float normalizeDegrees(float angleDeg) { + float out = angleDeg % 360.0f; + return out < 0.0f ? out + 360.0f : out; + } + public static int mixArgb(int start, int end, float t) { + int sa = (start >>> 24) & 0xFF; + int sr = (start >>> 16) & 0xFF; + int sg = (start >>> 8) & 0xFF; + int sb = start & 0xFF; + int ea = (end >>> 24) & 0xFF; + int er = (end >>> 16) & 0xFF; + int eg = (end >>> 8) & 0xFF; + int eb = end & 0xFF; + int a = (int) (sa + (ea - sa) * t + 0.5f); + int r = (int) (sr + (er - sr) * t + 0.5f); + int g = (int) (sg + (eg - sg) * t + 0.5f); + int b = (int) (sb + (eb - sb) * t + 0.5f); + return (a << 24) | (r << 16) | (g << 8) | b; + } + public static void roundedRectAnchor(double[] out, int offset, double x, double y, double width, double height, double radius, double targetX, double targetY) { + double cx = x + width * 0.5; + double cy = y + height * 0.5; + double halfW = Math.abs(width) * 0.5; + double halfH = Math.abs(height) * 0.5; + if (halfW <= 0.0 || halfH <= 0.0) { out[offset] = cx; out[offset + 1] = cy; return; } + double dx = targetX - cx; + double dy = targetY - cy; + if (Math.abs(dx) <= 0.0001 && Math.abs(dy) <= 0.0001) { out[offset] = cx; out[offset + 1] = cy; return; } + double r = Math.max(0.0, Math.min(Math.min(halfW, halfH), radius)); + double innerW = halfW - r; + double innerH = halfH - r; + double bestT = Double.POSITIVE_INFINITY; + if (Math.abs(dx) > 0.0001) { + double tx = (dx > 0.0 ? halfW : -halfW) / dx; + double yy = dy * tx; + if (tx > 0.0 && Math.abs(yy) <= innerH + 0.0001) bestT = Math.min(bestT, tx); + } + if (Math.abs(dy) > 0.0001) { + double ty = (dy > 0.0 ? halfH : -halfH) / dy; + double xx = dx * ty; + if (ty > 0.0 && Math.abs(xx) <= innerW + 0.0001) bestT = Math.min(bestT, ty); + } + if (r > 0.0) { + double circleX = dx >= 0.0 ? innerW : -innerW; + double circleY = dy >= 0.0 ? innerH : -innerH; + double a = dx * dx + dy * dy; + double b = -2.0 * (dx * circleX + dy * circleY); + double c = circleX * circleX + circleY * circleY - r * r; + double discriminant = b * b - 4.0 * a * c; + if (a > 0.0001 && discriminant >= 0.0) { + double sqrt = Math.sqrt(discriminant); + double t0 = (-b - sqrt) / (2.0 * a); + double t1 = (-b + sqrt) / (2.0 * a); + if (t0 > 0.0) bestT = Math.min(bestT, t0); + if (t1 > 0.0) bestT = Math.min(bestT, t1); + } + } + if (!Double.isFinite(bestT)) { + double tx = Math.abs(dx) > 0.0001 ? halfW / Math.abs(dx) : Double.POSITIVE_INFINITY; + double ty = Math.abs(dy) > 0.0001 ? halfH / Math.abs(dy) : Double.POSITIVE_INFINITY; + bestT = Math.min(tx, ty); + } + out[offset] = cx + dx * bestT; + out[offset + 1] = cy + dy * bestT; + } + public static int buildBezier(double[] out, double x1, double y1, double cx1, double cy1, double cx2, double cy2, double x2, double y2, int segments) { + int count = Math.max(2, Math.min(32, segments + 1)); + for (int i = 0; i < count; i++) { + double t = (double) i / (count - 1); + double u = 1.0 - t; + double x = u * u * u * x1 + 3.0 * u * u * t * cx1 + 3.0 * u * t * t * cx2 + t * t * t * x2; + double y = u * u * u * y1 + 3.0 * u * u * t * cy1 + 3.0 * u * t * t * cy2 + t * t * t * y2; + out[i * 2] = x; + out[i * 2 + 1] = y; + } + return count; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/geometry/UiRectGeometry.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/geometry/UiRectGeometry.java new file mode 100644 index 0000000..092657f --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/geometry/UiRectGeometry.java @@ -0,0 +1,94 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ +package dev.loki.lovisual.render.engine.renderer.ui.draw.edge; + +public final class UiRectGeometry { + private UiRectGeometry() {} + public static float clampRoundedRadius(float radius, double width, double height) { + float maxRadius = maxRoundedRadius(width, height); + if (radius < 0.0f) return 0.0f; + return Math.min(radius, maxRadius); + } + public static float clampChamfer(double chamfer, double width, double height) { + float max = maxRoundedRadius(width, height); + if (chamfer < 0.0) return 0.0f; + return (float) Math.min(chamfer, max); + } + public static float clampChamferAxis(double chamfer, double length) { + double max = Math.abs(length); + if (chamfer < 0.0 || max <= 0.0) return 0.0f; + return (float) Math.min(chamfer, max); + } + static float maxRoundedRadius(double width, double height) { + double w = Math.abs(width); + double h = Math.abs(height); + if (w <= 0.0 || h <= 0.0) return 0.0f; + return (float) (Math.min(w, h) * 0.5); + } + public static void normalizeCornerRadii(double width, double height, float radiusTL, float radiusTR, float radiusBR, float radiusBL, float[] out) { + if (out == null || out.length < 4) throw new IllegalArgumentException("Output array must have length >= 4"); + float w = (float) Math.abs(width); + float h = (float) Math.abs(height); + if (w <= 0.0f || h <= 0.0f) { out[0]=0.0f; out[1]=0.0f; out[2]=0.0f; out[3]=0.0f; return; } + float maxRadius = Math.min(w, h) * 0.5f; + float tl = Math.max(0.0f, Math.min(radiusTL, maxRadius)); + float tr = Math.max(0.0f, Math.min(radiusTR, maxRadius)); + float br = Math.max(0.0f, Math.min(radiusBR, maxRadius)); + float bl = Math.max(0.0f, Math.min(radiusBL, maxRadius)); + float scale = 1.0f; + float top = tl + tr; + if (top > w && top > 0.0f) scale = Math.min(scale, w / top); + float bottom = bl + br; + if (bottom > w && bottom > 0.0f) scale = Math.min(scale, w / bottom); + float left = tl + bl; + if (left > h && left > 0.0f) scale = Math.min(scale, h / left); + float right = tr + br; + if (right > h && right > 0.0f) scale = Math.min(scale, h / right); + if (scale < 1.0f) { tl *= scale; tr *= scale; br *= scale; bl *= scale; } + out[0]=tl; out[1]=tr; out[2]=br; out[3]=bl; + } + public static void normalizeChamfers(double width, double height, float chamferTL, float chamferTR, float chamferBR, float chamferBL, float[] out) { + if (out == null || out.length < 4) throw new IllegalArgumentException("Output array must have length >= 4"); + float w = (float) Math.abs(width); + float h = (float) Math.abs(height); + if (w <= 0.0f || h <= 0.0f) { out[0]=0.0f; out[1]=0.0f; out[2]=0.0f; out[3]=0.0f; return; } + float max = Math.min(w, h); + float tl = Math.max(0.0f, Math.min(chamferTL, max)); + float tr = Math.max(0.0f, Math.min(chamferTR, max)); + float br = Math.max(0.0f, Math.min(chamferBR, max)); + float bl = Math.max(0.0f, Math.min(chamferBL, max)); + float scale = 1.0f; + float top = tl + tr; + if (top > w && top > 0.0f) scale = Math.min(scale, w / top); + float bottom = bl + br; + if (bottom > w && bottom > 0.0f) scale = Math.min(scale, w / bottom); + float left = tl + bl; + if (left > h && left > 0.0f) scale = Math.min(scale, h / left); + float right = tr + br; + if (right > h && right > 0.0f) scale = Math.min(scale, h / right); + if (scale < 1.0f) { tl *= scale; tr *= scale; br *= scale; bl *= scale; } + out[0]=tl; out[1]=tr; out[2]=br; out[3]=bl; + } + static int buildChamferedRect(double[] out, double x, double y, double width, double height, double chamfer) { + double c = Math.max(0.0, Math.min(Math.min(Math.abs(width), Math.abs(height)) * 0.5, chamfer)); + double x2 = x + width; + double y2 = y + height; + out[0]=x+c; out[1]=y; out[2]=x2-c; out[3]=y; out[4]=x2; out[5]=y+c; out[6]=x2; out[7]=y2-c; + out[8]=x2-c; out[9]=y2; out[10]=x+c; out[11]=y2; out[12]=x; out[13]=y2-c; out[14]=x; out[15]=y+c; + return 8; + } + public static int buildNotchedRect(double[] out, double x, double y, double width, double height, double notchWidth, double notchDepth) { + double nw = Math.max(0.0, Math.min(Math.abs(width) * 0.5, notchWidth)); + double nd = Math.max(0.0, Math.min(Math.abs(height) * 0.5, notchDepth)); + double x2 = x + width; + double y2 = y + height; + double cx = x + width * 0.5; + out[0]=x; out[1]=y; out[2]=cx-nw; out[3]=y; out[4]=cx-nw; out[5]=y+nd; out[6]=cx+nw; out[7]=y+nd; + out[8]=cx+nw; out[9]=y; out[10]=x2; out[11]=y; out[12]=x2; out[13]=y2; out[14]=x; out[15]=y2; + return 8; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/mesh/UiPolygonTessellator.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/mesh/UiPolygonTessellator.java new file mode 100644 index 0000000..8150a65 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/mesh/UiPolygonTessellator.java @@ -0,0 +1,117 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ +package dev.loki.lovisual.render.engine.renderer.ui.draw.edge; + +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; + +public final class UiPolygonTessellator { + private UiPolygonTessellator() {} + public static void appendColoredQuad(MeshBuilder mesh, double x, double y, double width, double height, int cTopLeft, int cTopRight, int cBottomRight, int cBottomLeft) { + mesh.ensureQuadCapacity(); + int tlA=(cTopLeft>>>24)&0xFF; int tlR=(cTopLeft>>>16)&0xFF; int tlG=(cTopLeft>>>8)&0xFF; int tlB=cTopLeft&0xFF; + int trA=(cTopRight>>>24)&0xFF; int trR=(cTopRight>>>16)&0xFF; int trG=(cTopRight>>>8)&0xFF; int trB=cTopRight&0xFF; + int brA=(cBottomRight>>>24)&0xFF; int brR=(cBottomRight>>>16)&0xFF; int brG=(cBottomRight>>>8)&0xFF; int brB=cBottomRight&0xFF; + int blA=(cBottomLeft>>>24)&0xFF; int blR=(cBottomLeft>>>16)&0xFF; int blG=(cBottomLeft>>>8)&0xFF; int blB=cBottomLeft&0xFF; + int i1=mesh.vec2(x,y).color(tlR,tlG,tlB,tlA).next(); + int i2=mesh.vec2(x,y+height).color(blR,blG,blB,blA).next(); + int i3=mesh.vec2(x+width,y+height).color(brR,brG,brB,brA).next(); + int i4=mesh.vec2(x+width,y).color(trR,trG,trB,trA).next(); + mesh.quad(i1,i2,i3,i4); + } + static void emitUiPolygonTriangle(MeshBuilder mesh, int a, int b, int c, boolean reverseForUiCull) { + if (reverseForUiCull) mesh.triangle(a,c,b); else mesh.triangle(a,b,c); + } + public static void appendPolygon(MeshBuilder mesh, double[] points, int pointCount, int[] indices, int[] vertexIds, int argb) { + int safeCount=Math.min(pointCount, Math.min(points.length/2, Math.min(indices.length, vertexIds.length))); + int triangles=safeCount-2; + if (triangles<=0) return; + mesh.ensureCapacity(safeCount, triangles*3); + int a=(argb>>>24)&0xFF; int r=(argb>>>16)&0xFF; int g=(argb>>>8)&0xFF; int b=argb&0xFF; + for (int i=0;i=0.0; + if (isConvex(points,safeCount)) { for (int i=2;i=0.0?1.0:-1.0; + int guard=safeCount*safeCount; + while (remaining>2 && guard-->0) { + boolean clipped=false; + for (int i=0;i>>24)&0xFF; int r=(argb>>>16)&0xFF; int g=(argb>>>8)&0xFF; int b=argb&0xFF; + vertexIds[i]=mesh.vec2(points[i*2], points[i*2+1]).color(r,g,b,a).next(); + } + double area=polygonArea(points,safeCount); + boolean reverseForUiCull=area>=0.0; + if (isConvex(points,safeCount)) { for (int i=2;i=0.0?1.0:-1.0; + int guard=safeCount*safeCount; + while (remaining>2 && guard-->0) { + boolean clipped=false; + for (int i=0;i=0.0 && c2*winding>=0.0 && c3*winding>=0.0; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/mesh/UiPolylineGeometry.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/mesh/UiPolylineGeometry.java new file mode 100644 index 0000000..54ec4bf --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/edge/mesh/UiPolylineGeometry.java @@ -0,0 +1,97 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ +package dev.loki.lovisual.render.engine.renderer.ui.draw.edge; + +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; + +public final class UiPolylineGeometry { + private UiPolylineGeometry() {} + public static void appendPolyline(MeshBuilder mesh, double[] points, int pointCount, double thickness, boolean closed, int argb, boolean roundCapsAndJoins) { + int safeCount=Math.min(pointCount, points.length/2); + int segments=closed?safeCount:safeCount-1; + if (segments<=0) return; + mesh.ensureCapacity(segments*4, segments*6); + for (int i=0;i=safeCount?0:i+1; + appendThickSegment(mesh, points[i*2], points[i*2+1], points[next*2], points[next*2+1], thickness, argb); + } + if (!roundCapsAndJoins) return; + double radius=thickness*0.5; + if (closed) { for (int i=0;i=safeCount?0:i+1; + float t0=segments<=1?0.0f:(float)i/(float)segments; + float t1=segments<=1?1.0f:(float)(i+1)/(float)segments; + appendThickSegmentGradient(mesh, points[i*2], points[i*2+1], points[next*2], points[next*2+1], thickness, UiGradientGeometry.mixArgb(startArgb,endArgb,UiGradientGeometry.clamp01(t0)), UiGradientGeometry.mixArgb(startArgb,endArgb,UiGradientGeometry.clamp01(t1))); + } + if (!roundCapsAndJoins) return; + double radius=thickness*0.5; + for (int i=0;i=safeCount?0:i+1; + int c0=UiGradientGeometry.linearGradientColorAt(points[i*2]-x, points[i*2+1]-y, width, height, startArgb, endArgb, angleDeg, offsetPx); + int c1=UiGradientGeometry.linearGradientColorAt(points[next*2]-x, points[next*2+1]-y, width, height, startArgb, endArgb, angleDeg, offsetPx); + appendThickSegmentGradient(mesh, points[i*2], points[i*2+1], points[next*2], points[next*2+1], thickness, c0, c1); + } + if (!roundCapsAndJoins) return; + double radius=thickness*0.5; + for (int i=0;i>>24)&0xFF; int r=(argb>>>16)&0xFF; int g=(argb>>>8)&0xFF; int b=argb&0xFF; + int i1=mesh.vec2(x1-nx,y1-ny).color(r,g,b,a).next(); + int i2=mesh.vec2(x1+nx,y1+ny).color(r,g,b,a).next(); + int i3=mesh.vec2(x2+nx,y2+ny).color(r,g,b,a).next(); + int i4=mesh.vec2(x2-nx,y2-ny).color(r,g,b,a).next(); + mesh.quad(i1,i2,i3,i4); + } + static void appendThickSegmentGradient(MeshBuilder mesh, double x1, double y1, double x2, double y2, double thickness, int startArgb, int endArgb) { + double dx=x2-x1; double dy=y2-y1; double len=Math.sqrt(dx*dx+dy*dy); + if (len<=0.0001) return; + double half=thickness*0.5; double nx=-dy/len*half; double ny=dx/len*half; + int sa=(startArgb>>>24)&0xFF; int sr=(startArgb>>>16)&0xFF; int sg=(startArgb>>>8)&0xFF; int sb=startArgb&0xFF; + int ea=(endArgb>>>24)&0xFF; int er=(endArgb>>>16)&0xFF; int eg=(endArgb>>>8)&0xFF; int eb=endArgb&0xFF; + int i1=mesh.vec2(x1-nx,y1-ny).color(sr,sg,sb,sa).next(); + int i2=mesh.vec2(x1+nx,y1+ny).color(sr,sg,sb,sa).next(); + int i3=mesh.vec2(x2+nx,y2+ny).color(er,eg,eb,ea).next(); + int i4=mesh.vec2(x2-nx,y2-ny).color(er,eg,eb,ea).next(); + mesh.quad(i1,i2,i3,i4); + } + static void appendDisk(MeshBuilder mesh, double cx, double cy, double radius, int segments, int argb) { + if (radius<=0.0) return; + int safeSegments=Math.max(6, Math.min(24, segments)); + mesh.ensureCapacity(safeSegments+1, safeSegments*3); + int a=(argb>>>24)&0xFF; int r=(argb>>>16)&0xFF; int g=(argb>>>8)&0xFF; int b=argb&0xFF; + int center=mesh.vec2(cx,cy).color(r,g,b,a).next(); + int first=-1; int prev=-1; + for (int i=0;i=0 && prev>=0) mesh.triangle(center,prev,first); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/effect/UiEffectKind.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/effect/UiEffectKind.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/effect/UiEffectKind.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/effect/UiEffectKind.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/effect/UiEffectSpec.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/effect/UiEffectSpec.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/effect/UiEffectSpec.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/effect/UiEffectSpec.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/effect/UiPathCap.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/effect/UiPathCap.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/effect/UiPathCap.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/effect/UiPathCap.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/effect/UiPathJoin.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/effect/UiPathJoin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/effect/UiPathJoin.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/effect/UiPathJoin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/paint/UiPaint.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/paint/UiPaint.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/paint/UiPaint.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/paint/UiPaint.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/paint/UiPaintKind.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/paint/UiPaintKind.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/paint/UiPaintKind.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/paint/UiPaintKind.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/paint/UiPaints.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/paint/UiPaints.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/paint/UiPaints.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/paint/UiPaints.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/shape/UiShape.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/shape/UiShape.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/shape/UiShape.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/shape/UiShape.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/shape/UiShapeKind.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/shape/UiShapeKind.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/shape/UiShapeKind.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/shape/UiShapeKind.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/shape/UiShapeShaderParams.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/shape/UiShapeShaderParams.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/shape/UiShapeShaderParams.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/shape/UiShapeShaderParams.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/shape/UiShapes.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/shape/UiShapes.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/shape/UiShapes.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/draw/shape/UiShapes.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/ItemBatchRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/ItemBatchRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/ItemBatchRenderer.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/ItemBatchRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/ItemDrawCommand.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/ItemDrawCommand.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/ItemDrawCommand.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/ItemDrawCommand.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/ItemResolveKey.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/ItemResolveKey.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/ItemResolveKey.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/ItemResolveKey.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/UiItemSubmission.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/UiItemSubmission.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/UiItemSubmission.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/item/UiItemSubmission.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/pass/UiEffectGraph.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/pass/UiEffectGraph.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/pass/UiEffectGraph.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/pass/UiEffectGraph.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/pass/UiPassCompiler.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/pass/UiPassCompiler.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/pass/UiPassCompiler.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/pass/UiPassCompiler.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/pass/UiPassExecutor.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/pass/UiPassExecutor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/pass/UiPassExecutor.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/pass/UiPassExecutor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/action/UiAction.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/action/UiAction.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/action/UiAction.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/action/UiAction.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/action/UiActionContext.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/action/UiActionContext.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/action/UiActionContext.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/action/UiActionContext.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/action/UiActionRef.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/action/UiActionRef.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/action/UiActionRef.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/action/UiActionRef.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/action/UiActionRegistry.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/action/UiActionRegistry.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/action/UiActionRegistry.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/action/UiActionRegistry.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/animation/UiAnimationSpec.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/animation/UiAnimationSpec.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/animation/UiAnimationSpec.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/animation/UiAnimationSpec.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/animation/UiAnimationState.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/animation/UiAnimationState.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/animation/UiAnimationState.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/animation/UiAnimationState.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/animation/UiEasing.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/animation/UiEasing.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/animation/UiEasing.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/animation/UiEasing.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/asset/UiAssetProvider.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/asset/UiAssetProvider.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/asset/UiAssetProvider.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/asset/UiAssetProvider.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/asset/UiAssetRef.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/asset/UiAssetRef.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/asset/UiAssetRef.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/asset/UiAssetRef.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/asset/UiAssetResolver.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/asset/UiAssetResolver.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/asset/UiAssetResolver.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/asset/UiAssetResolver.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/asset/registry/UiAssetKind.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/asset/registry/UiAssetKind.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/asset/registry/UiAssetKind.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/asset/registry/UiAssetKind.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/asset/registry/UiAssetRegistry.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/asset/registry/UiAssetRegistry.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/asset/registry/UiAssetRegistry.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/asset/registry/UiAssetRegistry.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/UiBounds.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/UiBounds.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/UiBounds.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/UiBounds.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/UiDocument.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/UiDocument.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/UiDocument.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/UiDocument.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/UiRuntimeException.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/UiRuntimeException.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/UiRuntimeException.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/UiRuntimeException.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/component/UiComponentFactory.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/component/UiComponentFactory.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/component/UiComponentFactory.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/component/UiComponentFactory.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/component/UiComponentRegistry.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/component/UiComponentRegistry.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/component/UiComponentRegistry.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/component/UiComponentRegistry.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/node/UiNode.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/node/UiNode.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/node/UiNode.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/node/UiNode.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/node/UiNodeSpec.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/node/UiNodeSpec.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/node/UiNodeSpec.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/node/UiNodeSpec.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/node/UiNodeType.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/node/UiNodeType.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/node/UiNodeType.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/node/UiNodeType.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/node/UiProps.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/node/UiProps.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/node/UiProps.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/node/UiProps.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/runtime/UiLifecycle.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/runtime/UiLifecycle.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/runtime/UiLifecycle.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/runtime/UiLifecycle.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/runtime/UiRuntime.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/runtime/UiRuntime.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/runtime/UiRuntime.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/runtime/UiRuntime.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/runtime/UiState.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/runtime/UiState.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/runtime/UiState.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/core/runtime/UiState.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/debug/UiPerfCounters.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/debug/UiPerfCounters.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/debug/UiPerfCounters.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/debug/UiPerfCounters.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/debug/UiRuntimeDiagnostics.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/debug/UiRuntimeDiagnostics.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/debug/UiRuntimeDiagnostics.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/debug/UiRuntimeDiagnostics.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/debug/UiTreeDumper.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/debug/UiTreeDumper.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/debug/UiTreeDumper.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/debug/UiTreeDumper.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/error/UiErrorPolicy.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/error/UiErrorPolicy.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/error/UiErrorPolicy.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/error/UiErrorPolicy.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/UiHitTester.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/UiHitTester.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/UiHitTester.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/UiHitTester.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/UiInputDispatcher.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/UiInputDispatcher.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/UiInputDispatcher.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/UiInputDispatcher.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/UiInputState.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/UiInputState.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/UiInputState.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/UiInputState.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/event/UiHitResult.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/event/UiHitResult.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/event/UiHitResult.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/event/UiHitResult.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/event/UiKeyboardEvent.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/event/UiKeyboardEvent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/event/UiKeyboardEvent.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/event/UiKeyboardEvent.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/event/UiPointerEvent.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/event/UiPointerEvent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/event/UiPointerEvent.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/event/UiPointerEvent.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/event/UiScrollEvent.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/event/UiScrollEvent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/event/UiScrollEvent.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/event/UiScrollEvent.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/focus/UiCursorManager.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/focus/UiCursorManager.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/focus/UiCursorManager.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/focus/UiCursorManager.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/focus/UiFocusManager.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/focus/UiFocusManager.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/focus/UiFocusManager.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/input/focus/UiFocusManager.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/layout/UiLayoutEngine.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/layout/UiLayoutEngine.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/layout/UiLayoutEngine.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/layout/UiLayoutEngine.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/reconcile/UiDiffStats.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/reconcile/UiDiffStats.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/reconcile/UiDiffStats.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/reconcile/UiDiffStats.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/reconcile/UiReconcileContext.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/reconcile/UiReconcileContext.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/reconcile/UiReconcileContext.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/reconcile/UiReconcileContext.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/reconcile/UiReconciler.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/reconcile/UiReconciler.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/reconcile/UiReconciler.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/reconcile/UiReconciler.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/reconcile/match/UiKeyPath.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/reconcile/match/UiKeyPath.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/reconcile/match/UiKeyPath.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/reconcile/match/UiKeyPath.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/reconcile/match/UiNodeMatcher.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/reconcile/match/UiNodeMatcher.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/reconcile/match/UiNodeMatcher.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/reconcile/match/UiNodeMatcher.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/bridge/UiBlendSpec.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/bridge/UiBlendSpec.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/bridge/UiBlendSpec.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/bridge/UiBlendSpec.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/bridge/UiClipStack.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/bridge/UiClipStack.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/bridge/UiClipStack.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/bridge/UiClipStack.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/bridge/UiImageRendererBridge.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/bridge/UiImageRendererBridge.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/bridge/UiImageRendererBridge.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/bridge/UiImageRendererBridge.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/bridge/UiItemRendererBridge.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/bridge/UiItemRendererBridge.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/bridge/UiItemRendererBridge.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/bridge/UiItemRendererBridge.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/context/UiProjectionMode.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/context/UiProjectionMode.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/context/UiProjectionMode.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/context/UiProjectionMode.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/context/UiRenderContext.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/context/UiRenderContext.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/context/UiRenderContext.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/context/UiRenderContext.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/primitive/UiGlowPainter.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/primitive/UiGlowPainter.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/primitive/UiGlowPainter.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/primitive/UiGlowPainter.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/primitive/UiShapeContext.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/primitive/UiShapeContext.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/primitive/UiShapeContext.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/primitive/UiShapeContext.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/primitive/UiShapePainter.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/primitive/UiShapePainter.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/primitive/UiShapePainter.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/primitive/UiShapePainter.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/primitive/UiShapeProps.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/primitive/UiShapeProps.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/primitive/UiShapeProps.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/primitive/UiShapeProps.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/rect/UiCornerPainter.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/rect/UiCornerPainter.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/rect/UiCornerPainter.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/rect/UiCornerPainter.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/rect/UiRectPainter.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/rect/UiRectPainter.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/rect/UiRectPainter.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/rect/UiRectPainter.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/render/UiPrimitiveRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/render/UiPrimitiveRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/render/UiPrimitiveRenderer.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/render/UiPrimitiveRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/render/UiRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/render/UiRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/render/UiRenderer.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/render/UiRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/render/UiTextEffectRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/render/UiTextEffectRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/render/UiTextEffectRenderer.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/render/UiTextEffectRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/render/UiTextRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/render/UiTextRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/render/UiTextRenderer.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/render/render/UiTextRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/engine/JavetNativeResourceLoader.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/engine/JavetNativeResourceLoader.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/engine/JavetNativeResourceLoader.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/engine/JavetNativeResourceLoader.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/engine/JavetUiScriptEngine.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/engine/JavetUiScriptEngine.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/engine/JavetUiScriptEngine.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/engine/JavetUiScriptEngine.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/engine/UiScriptEngine.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/engine/UiScriptEngine.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/engine/UiScriptEngine.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/engine/UiScriptEngine.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/engine/UiScriptEngineProvider.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/engine/UiScriptEngineProvider.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/engine/UiScriptEngineProvider.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/engine/UiScriptEngineProvider.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/module/UiScriptModule.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/module/UiScriptModule.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/module/UiScriptModule.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/module/UiScriptModule.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/module/UiScriptModuleHandle.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/module/UiScriptModuleHandle.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/module/UiScriptModuleHandle.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/module/UiScriptModuleHandle.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/module/UiScriptModuleId.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/module/UiScriptModuleId.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/module/UiScriptModuleId.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/module/UiScriptModuleId.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/module/UiScriptModuleLoader.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/module/UiScriptModuleLoader.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/module/UiScriptModuleLoader.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/module/UiScriptModuleLoader.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/patch/UiBoundsPatchSet.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/patch/UiBoundsPatchSet.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/patch/UiBoundsPatchSet.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/patch/UiBoundsPatchSet.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/patch/UiScriptColor.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/patch/UiScriptColor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/patch/UiScriptColor.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/patch/UiScriptColor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/patch/UiScriptComponent.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/patch/UiScriptComponent.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/patch/UiScriptComponent.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/patch/UiScriptComponent.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/patch/UiScriptPatchSet.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/patch/UiScriptPatchSet.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/patch/UiScriptPatchSet.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/patch/UiScriptPatchSet.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/registry/UiScriptModuleRegistry.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/registry/UiScriptModuleRegistry.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/registry/UiScriptModuleRegistry.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/registry/UiScriptModuleRegistry.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/registry/UiScriptModuleTransform.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/registry/UiScriptModuleTransform.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/registry/UiScriptModuleTransform.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/registry/UiScriptModuleTransform.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/registry/UiTypeScriptStripper.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/registry/UiTypeScriptStripper.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/registry/UiTypeScriptStripper.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/registry/UiTypeScriptStripper.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/render/UiScriptObjectConverter.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/render/UiScriptObjectConverter.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/render/UiScriptObjectConverter.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/render/UiScriptObjectConverter.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/render/UiScriptProps.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/render/UiScriptProps.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/render/UiScriptProps.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/render/UiScriptProps.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/render/UiScriptRenderContext.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/render/UiScriptRenderContext.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/render/UiScriptRenderContext.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/render/UiScriptRenderContext.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/render/UiScriptRenderResult.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/render/UiScriptRenderResult.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/render/UiScriptRenderResult.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/render/UiScriptRenderResult.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/CachedUiScriptRuntime.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/CachedUiScriptRuntime.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/CachedUiScriptRuntime.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/CachedUiScriptRuntime.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/UiScriptRuntimeError.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/UiScriptRuntimeError.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/UiScriptRuntimeError.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/UiScriptRuntimeError.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/UiScriptRuntimeSource.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/UiScriptRuntimeSource.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/UiScriptRuntimeSource.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/UiScriptRuntimeSource.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/UiScriptSourceKind.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/UiScriptSourceKind.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/UiScriptSourceKind.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/runtime/UiScriptSourceKind.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/UiResolvedStyle.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/UiResolvedStyle.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/UiResolvedStyle.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/UiResolvedStyle.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/UiStyle.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/UiStyle.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/UiStyle.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/UiStyle.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/parse/UiStyleCache.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/parse/UiStyleCache.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/parse/UiStyleCache.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/parse/UiStyleCache.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/parse/UiStyleParser.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/parse/UiStyleParser.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/parse/UiStyleParser.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/parse/UiStyleParser.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/parse/UiStyleResolver.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/parse/UiStyleResolver.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/parse/UiStyleResolver.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/parse/UiStyleResolver.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/theme/UiColor.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/theme/UiColor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/theme/UiColor.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/theme/UiColor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/theme/UiTheme.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/theme/UiTheme.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/theme/UiTheme.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/theme/UiTheme.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/theme/UiThemeRegistry.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/theme/UiThemeRegistry.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/theme/UiThemeRegistry.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/theme/UiThemeRegistry.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/token/UiAlign.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/token/UiAlign.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/token/UiAlign.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/token/UiAlign.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/token/UiJustify.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/token/UiJustify.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/token/UiJustify.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/token/UiJustify.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/token/UiOverflow.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/token/UiOverflow.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/token/UiOverflow.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/token/UiOverflow.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/token/UiStyleToken.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/token/UiStyleToken.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/token/UiStyleToken.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/style/token/UiStyleToken.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/warp/RenderWarp.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/warp/RenderWarp.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/warp/RenderWarp.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/warp/RenderWarp.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/warp/RenderWarpStack.java b/mod/src/main/java/dev/loki/lovisual/render/engine/renderer/warp/RenderWarpStack.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/warp/RenderWarpStack.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/renderer/warp/RenderWarpStack.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/SodiumGlBackend.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/SodiumGlBackend.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/SodiumGlBackend.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/SodiumGlBackend.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/GlBackendAccess.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/GlBackendAccess.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/GlBackendAccess.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/GlBackendAccess.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/GlStencilFramebufferSupport.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/GlStencilFramebufferSupport.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/GlStencilFramebufferSupport.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/GlStencilFramebufferSupport.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/GlStencilShapeClipBackend.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/GlStencilShapeClipBackend.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/GlStencilShapeClipBackend.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/GlStencilShapeClipBackend.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/state/SodiumGlPipelineStateBackend.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/state/SodiumGlPipelineStateBackend.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/state/SodiumGlPipelineStateBackend.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/state/SodiumGlPipelineStateBackend.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/LoVisualVulkanBackend.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/LoVisualVulkanBackend.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/LoVisualVulkanBackend.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/LoVisualVulkanBackend.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/clip/VulkanShapeClipBackend.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/clip/VulkanShapeClipBackend.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/clip/VulkanShapeClipBackend.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/clip/VulkanShapeClipBackend.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/clip/VulkanShapeClipBridge.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/clip/VulkanShapeClipBridge.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/clip/VulkanShapeClipBridge.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/clip/VulkanShapeClipBridge.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/clip/VulkanStencilAttachmentManager.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/clip/VulkanStencilAttachmentManager.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/clip/VulkanStencilAttachmentManager.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/clip/VulkanStencilAttachmentManager.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/msaa/VulkanMsaaControl.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/msaa/VulkanMsaaControl.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/msaa/VulkanMsaaControl.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/msaa/VulkanMsaaControl.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/msaa/VulkanMsaaResolveBridge.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/msaa/VulkanMsaaResolveBridge.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/msaa/VulkanMsaaResolveBridge.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/msaa/VulkanMsaaResolveBridge.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/state/VulkanPipelineStateBackend.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/state/VulkanPipelineStateBackend.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/state/VulkanPipelineStateBackend.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/state/VulkanPipelineStateBackend.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/util/VulkanRenderStateBridge.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/util/VulkanRenderStateBridge.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/util/VulkanRenderStateBridge.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/util/VulkanRenderStateBridge.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/util/VulkanRuntimeGuards.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/util/VulkanRuntimeGuards.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/util/VulkanRuntimeGuards.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/util/VulkanRuntimeGuards.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/blit/Blaze3dTextureBlitter.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/blit/Blaze3dTextureBlitter.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/blit/Blaze3dTextureBlitter.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/blit/Blaze3dTextureBlitter.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/blit/TextureBlitter.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/blit/TextureBlitter.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/blit/TextureBlitter.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/blit/TextureBlitter.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/clip/ShapeClipBackend.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/clip/ShapeClipBackend.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/clip/ShapeClipBackend.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/clip/ShapeClipBackend.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/clip/ShapeClipRenderPassContract.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/clip/ShapeClipRenderPassContract.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/clip/ShapeClipRenderPassContract.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/clip/ShapeClipRenderPassContract.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/command/FullscreenDrawCommand.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/command/FullscreenDrawCommand.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/command/FullscreenDrawCommand.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/command/FullscreenDrawCommand.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/command/RhiDrawCommand.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/command/RhiDrawCommand.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/command/RhiDrawCommand.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/command/RhiDrawCommand.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/command/RhiSamplerBinding.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/command/RhiSamplerBinding.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/command/RhiSamplerBinding.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/command/RhiSamplerBinding.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/command/RhiUniformBinding.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/command/RhiUniformBinding.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/command/RhiUniformBinding.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/command/RhiUniformBinding.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/fullscreen/Blaze3dFullscreenBackend.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/fullscreen/Blaze3dFullscreenBackend.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/fullscreen/Blaze3dFullscreenBackend.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/fullscreen/Blaze3dFullscreenBackend.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/fullscreen/FullscreenBackend.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/fullscreen/FullscreenBackend.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/fullscreen/FullscreenBackend.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/fullscreen/FullscreenBackend.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/msaa/MsaaControl.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/msaa/MsaaControl.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/msaa/MsaaControl.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/msaa/MsaaControl.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/msaa/SodiumGlMsaaControl.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/msaa/SodiumGlMsaaControl.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/msaa/SodiumGlMsaaControl.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/msaa/SodiumGlMsaaControl.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/depth/BlendPolicy.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/depth/BlendPolicy.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/depth/BlendPolicy.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/depth/BlendPolicy.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/depth/CullPolicy.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/depth/CullPolicy.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/depth/CullPolicy.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/depth/CullPolicy.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/depth/DepthPolicy.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/depth/DepthPolicy.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/depth/DepthPolicy.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/depth/DepthPolicy.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/depth/FogPolicy.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/depth/FogPolicy.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/depth/FogPolicy.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/depth/FogPolicy.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/layout/RenderPipelineRegistry.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/layout/RenderPipelineRegistry.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/layout/RenderPipelineRegistry.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/layout/RenderPipelineRegistry.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/layout/UniformLayoutSpec.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/layout/UniformLayoutSpec.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/layout/UniformLayoutSpec.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/layout/UniformLayoutSpec.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/layout/VertexLayoutSpec.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/layout/VertexLayoutSpec.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/layout/VertexLayoutSpec.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/layout/VertexLayoutSpec.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/pipeline/PipelineDomain.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/pipeline/PipelineDomain.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/pipeline/PipelineDomain.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/pipeline/PipelineDomain.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/pipeline/PipelineKey.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/pipeline/PipelineKey.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/pipeline/PipelineKey.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/pipeline/PipelineKey.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/pipeline/PipelineMetadata.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/pipeline/PipelineMetadata.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/pipeline/PipelineMetadata.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/pipeline/PipelineMetadata.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/pipeline/RenderPipelineSpec.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/pipeline/RenderPipelineSpec.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/pipeline/RenderPipelineSpec.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/pipeline/pipeline/RenderPipelineSpec.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/GpuMeshHandle.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/GpuMeshHandle.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/GpuMeshHandle.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/GpuMeshHandle.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/LoVisualRhi.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/LoVisualRhi.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/LoVisualRhi.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/LoVisualRhi.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/MeshOwnership.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/MeshOwnership.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/MeshOwnership.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/MeshOwnership.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/atlas/GlyphAtlasManager.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/atlas/GlyphAtlasManager.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/atlas/GlyphAtlasManager.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/atlas/GlyphAtlasManager.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/atlas/GlyphAtlasPage.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/atlas/GlyphAtlasPage.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/atlas/GlyphAtlasPage.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/atlas/GlyphAtlasPage.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/manager/FrameResourceRetirementQueue.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/manager/FrameResourceRetirementQueue.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/manager/FrameResourceRetirementQueue.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/manager/FrameResourceRetirementQueue.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/manager/RenderResourceManager.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/manager/RenderResourceManager.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/manager/RenderResourceManager.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/manager/RenderResourceManager.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/manager/RenderResourceStatsSnapshot.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/manager/RenderResourceStatsSnapshot.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/manager/RenderResourceStatsSnapshot.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/manager/RenderResourceStatsSnapshot.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/manager/ResourceLeakTracker.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/manager/ResourceLeakTracker.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/manager/ResourceLeakTracker.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/manager/ResourceLeakTracker.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/pool/FramebufferDescriptor.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/pool/FramebufferDescriptor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/pool/FramebufferDescriptor.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/pool/FramebufferDescriptor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/pool/FramebufferPool.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/pool/FramebufferPool.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/pool/FramebufferPool.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/pool/FramebufferPool.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/pool/SamplerCache.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/pool/SamplerCache.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/pool/SamplerCache.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/pool/SamplerCache.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/pool/TexturePool.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/pool/TexturePool.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/pool/TexturePool.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/resource/pool/TexturePool.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/scissor/GlobalScissorState.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/scissor/GlobalScissorState.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/scissor/GlobalScissorState.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/scissor/GlobalScissorState.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/state/PipelineStateBackend.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/state/PipelineStateBackend.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/state/PipelineStateBackend.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/state/PipelineStateBackend.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/stats/RhiStats.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/stats/RhiStats.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/stats/RhiStats.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/stats/RhiStats.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/stats/RhiStatsSnapshot.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/stats/RhiStatsSnapshot.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/stats/RhiStatsSnapshot.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/stats/RhiStatsSnapshot.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/uniform/FrameResource.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/uniform/FrameResource.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/uniform/FrameResource.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/uniform/FrameResource.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/uniform/LegacyUniformFrameResource.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/uniform/LegacyUniformFrameResource.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/uniform/LegacyUniformFrameResource.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/uniform/LegacyUniformFrameResource.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/uniform/LoVisualUniformAllocator.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/uniform/LoVisualUniformAllocator.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/uniform/LoVisualUniformAllocator.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/uniform/LoVisualUniformAllocator.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/uniform/stats/UniformAllocatorStats.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/uniform/stats/UniformAllocatorStats.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/uniform/stats/UniformAllocatorStats.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/uniform/stats/UniformAllocatorStats.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/uniform/stats/UniformAllocatorStatsSnapshot.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/uniform/stats/UniformAllocatorStatsSnapshot.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/uniform/stats/UniformAllocatorStatsSnapshot.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/uniform/stats/UniformAllocatorStatsSnapshot.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/upload/DynamicMeshBackend.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/upload/DynamicMeshBackend.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/upload/DynamicMeshBackend.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/upload/DynamicMeshBackend.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/upload/blaze3d/Blaze3dDynamicMeshBackend.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/upload/blaze3d/Blaze3dDynamicMeshBackend.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/upload/blaze3d/Blaze3dDynamicMeshBackend.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/upload/blaze3d/Blaze3dDynamicMeshBackend.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/upload/blaze3d/Blaze3dFrameFence.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/upload/blaze3d/Blaze3dFrameFence.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/upload/blaze3d/Blaze3dFrameFence.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/upload/blaze3d/Blaze3dFrameFence.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/upload/blaze3d/Blaze3dMeshAllocation.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/upload/blaze3d/Blaze3dMeshAllocation.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/upload/blaze3d/Blaze3dMeshAllocation.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/upload/blaze3d/Blaze3dMeshAllocation.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/upload/blaze3d/Blaze3dMeshArena.java b/mod/src/main/java/dev/loki/lovisual/render/engine/rhi/upload/blaze3d/Blaze3dMeshArena.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/upload/blaze3d/Blaze3dMeshArena.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/rhi/upload/blaze3d/Blaze3dMeshArena.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/shader/LoVisualShaderSources.java b/mod/src/main/java/dev/loki/lovisual/render/engine/shader/LoVisualShaderSources.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/shader/LoVisualShaderSources.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/shader/LoVisualShaderSources.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/backend/SvgInlineText.java b/mod/src/main/java/dev/loki/lovisual/render/engine/svg/backend/SvgInlineText.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/backend/SvgInlineText.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/svg/backend/SvgInlineText.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/backend/SvgMeshBackend.java b/mod/src/main/java/dev/loki/lovisual/render/engine/svg/backend/SvgMeshBackend.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/backend/SvgMeshBackend.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/svg/backend/SvgMeshBackend.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/backend/geometry/SvgGeometry.java b/mod/src/main/java/dev/loki/lovisual/render/engine/svg/backend/geometry/SvgGeometry.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/backend/geometry/SvgGeometry.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/svg/backend/geometry/SvgGeometry.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/backend/parser/SvgParser.java b/mod/src/main/java/dev/loki/lovisual/render/engine/svg/backend/parser/SvgParser.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/backend/parser/SvgParser.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/svg/backend/parser/SvgParser.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/backend/render/SvgRender.java b/mod/src/main/java/dev/loki/lovisual/render/engine/svg/backend/render/SvgRender.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/backend/render/SvgRender.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/svg/backend/render/SvgRender.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/backend/store/SvgCache.java b/mod/src/main/java/dev/loki/lovisual/render/engine/svg/backend/store/SvgCache.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/backend/store/SvgCache.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/svg/backend/store/SvgCache.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/backend/texture/SvgTexture.java b/mod/src/main/java/dev/loki/lovisual/render/engine/svg/backend/texture/SvgTexture.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/backend/texture/SvgTexture.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/svg/backend/texture/SvgTexture.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/model/SvgColorMode.java b/mod/src/main/java/dev/loki/lovisual/render/engine/svg/model/SvgColorMode.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/model/SvgColorMode.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/svg/model/SvgColorMode.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/model/SvgRenderOptions.java b/mod/src/main/java/dev/loki/lovisual/render/engine/svg/model/SvgRenderOptions.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/model/SvgRenderOptions.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/svg/model/SvgRenderOptions.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/model/SvgStyle.java b/mod/src/main/java/dev/loki/lovisual/render/engine/svg/model/SvgStyle.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/model/SvgStyle.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/svg/model/SvgStyle.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/model/SvgStyleUtil.java b/mod/src/main/java/dev/loki/lovisual/render/engine/svg/model/SvgStyleUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/model/SvgStyleUtil.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/svg/model/SvgStyleUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/parser/SvgMsdfRegistry.java b/mod/src/main/java/dev/loki/lovisual/render/engine/svg/parser/SvgMsdfRegistry.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/parser/SvgMsdfRegistry.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/svg/parser/SvgMsdfRegistry.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/parser/SvgPathParser.java b/mod/src/main/java/dev/loki/lovisual/render/engine/svg/parser/SvgPathParser.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/parser/SvgPathParser.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/svg/parser/SvgPathParser.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/parser/SvgRegistry.java b/mod/src/main/java/dev/loki/lovisual/render/engine/svg/parser/SvgRegistry.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/parser/SvgRegistry.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/svg/parser/SvgRegistry.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/backend/atlas/BitmapAtlasTextBackend.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/atlas/BitmapAtlasTextBackend.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/backend/atlas/BitmapAtlasTextBackend.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/atlas/BitmapAtlasTextBackend.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/backend/atlas/GlyphAtlasManager.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/atlas/GlyphAtlasManager.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/backend/atlas/GlyphAtlasManager.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/atlas/GlyphAtlasManager.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/backend/atlas/GlyphAtlasPage.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/atlas/GlyphAtlasPage.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/backend/atlas/GlyphAtlasPage.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/atlas/GlyphAtlasPage.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/backend/atlas/MsdfTextBackend.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/atlas/MsdfTextBackend.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/backend/atlas/MsdfTextBackend.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/atlas/MsdfTextBackend.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/backend/backend/TextBackend.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/backend/TextBackend.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/backend/backend/TextBackend.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/backend/TextBackend.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/backend/backend/TextBackendRouter.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/backend/TextBackendRouter.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/backend/backend/TextBackendRouter.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/backend/TextBackendRouter.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/backend/backend/TextDrawCommand.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/backend/TextDrawCommand.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/backend/backend/TextDrawCommand.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/backend/TextDrawCommand.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/backend/backend/VanillaSodiumTextBackend.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/backend/VanillaSodiumTextBackend.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/backend/backend/VanillaSodiumTextBackend.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/backend/VanillaSodiumTextBackend.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/backend/placement/TextPlacementMode.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/placement/TextPlacementMode.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/backend/placement/TextPlacementMode.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/placement/TextPlacementMode.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/backend/placement/TextPlacementResolver.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/placement/TextPlacementResolver.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/backend/placement/TextPlacementResolver.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/placement/TextPlacementResolver.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/backend/placement/TextPlacementTransform.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/placement/TextPlacementTransform.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/backend/placement/TextPlacementTransform.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/placement/TextPlacementTransform.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/backend/placement/TextRenderDomain.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/placement/TextRenderDomain.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/backend/placement/TextRenderDomain.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/backend/placement/TextRenderDomain.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/command/TextCommandBuffer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/command/TextCommandBuffer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/command/TextCommandBuffer.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/command/TextCommandBuffer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/command/TextCommandStats.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/command/TextCommandStats.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/command/TextCommandStats.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/command/TextCommandStats.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/command/TextCommandStatsSnapshot.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/command/TextCommandStatsSnapshot.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/command/TextCommandStatsSnapshot.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/command/TextCommandStatsSnapshot.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/command/TextEffectSpec.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/command/TextEffectSpec.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/command/TextEffectSpec.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/command/TextEffectSpec.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/font/Font.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/Font.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/font/Font.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/font/Font.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/font/FontFace.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/FontFace.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/font/FontFace.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/font/FontFace.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/font/FontUtils.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/FontUtils.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/font/FontUtils.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/font/FontUtils.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/font/Fonts.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/Fonts.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/font/Fonts.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/font/Fonts.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/font/faces/BuiltinFontFace.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/faces/BuiltinFontFace.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/font/faces/BuiltinFontFace.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/font/faces/BuiltinFontFace.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/font/faces/EmptyGlyphFont.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/faces/EmptyGlyphFont.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/font/faces/EmptyGlyphFont.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/font/faces/EmptyGlyphFont.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/font/faces/FontFamily.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/faces/FontFamily.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/font/faces/FontFamily.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/font/faces/FontFamily.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/font/faces/SystemFontFace.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/faces/SystemFontFace.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/font/faces/SystemFontFace.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/font/faces/SystemFontFace.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/font/info/FontDebugStats.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/info/FontDebugStats.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/font/info/FontDebugStats.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/font/info/FontDebugStats.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/font/info/FontInfo.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/info/FontInfo.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/font/info/FontInfo.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/font/info/FontInfo.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/font/info/TextBackendPreference.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/info/TextBackendPreference.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/font/info/TextBackendPreference.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/font/info/TextBackendPreference.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/font/info/TextClipSpec.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/font/info/TextClipSpec.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/font/info/TextClipSpec.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/font/info/TextClipSpec.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/glyph/CustomTextRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/CustomTextRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/glyph/CustomTextRenderer.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/CustomTextRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/glyph/GlyphFont.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/GlyphFont.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/glyph/GlyphFont.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/GlyphFont.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/glyph/GradientTexts.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/GradientTexts.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/glyph/GradientTexts.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/GradientTexts.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/glyph/VanillaTextRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/VanillaTextRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/glyph/VanillaTextRenderer.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/VanillaTextRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/glyph/custom/CustomFontSet.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/custom/CustomFontSet.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/glyph/custom/CustomFontSet.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/custom/CustomFontSet.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/glyph/custom/HorizontalFade.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/custom/HorizontalFade.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/glyph/custom/HorizontalFade.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/custom/HorizontalFade.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/glyph/custom/TextGlyphFallback.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/custom/TextGlyphFallback.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/glyph/custom/TextGlyphFallback.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/glyph/custom/TextGlyphFallback.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/msdf/MsdfFont.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/MsdfFont.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/msdf/MsdfFont.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/MsdfFont.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/msdf/MsdfGlyphDrawer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/MsdfGlyphDrawer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/msdf/MsdfGlyphDrawer.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/MsdfGlyphDrawer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/msdf/WorldTextRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/WorldTextRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/msdf/WorldTextRenderer.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/WorldTextRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfAtlas.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfAtlas.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfAtlas.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfAtlas.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfAtlasLoader.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfAtlasLoader.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfAtlasLoader.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfAtlasLoader.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfGlyph.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfGlyph.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfGlyph.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfGlyph.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfJson.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfJson.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfJson.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfJson.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/render/TextRenderSystem.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/render/TextRenderSystem.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/render/TextRenderSystem.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/render/TextRenderSystem.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/render/TextRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/text/render/TextRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/render/TextRenderer.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/text/render/TextRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/texture/Texture.java b/mod/src/main/java/dev/loki/lovisual/render/engine/texture/Texture.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/texture/Texture.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/texture/Texture.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/texture/TextureStorage.java b/mod/src/main/java/dev/loki/lovisual/render/engine/texture/TextureStorage.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/texture/TextureStorage.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/texture/TextureStorage.java diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/MeshBuilder.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/MeshBuilder.java new file mode 100644 index 0000000..a291702 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/MeshBuilder.java @@ -0,0 +1,447 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.render.engine.uniform; + +import com.mojang.blaze3d.buffers.GpuBuffer; +import com.mojang.blaze3d.pipeline.RenderPipeline; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.VertexFormat; +import net.minecraft.client.Minecraft; +import net.minecraft.world.phys.Vec3; +import dev.loki.lovisual.render.engine.RenderState; +import dev.loki.lovisual.render.engine.color.RenderColor; +import dev.loki.lovisual.render.engine.guard.LegacyRenderPath; +import dev.loki.lovisual.render.engine.guard.RenderArchitectureGuard; +import dev.loki.lovisual.render.engine.renderer.warp.RenderWarp; +import dev.loki.lovisual.render.engine.renderer.warp.RenderWarpStack; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Objects; + +import static org.lwjgl.system.MemoryUtil.memPutByte; +import static org.lwjgl.system.MemoryUtil.memPutFloat; +import static org.lwjgl.system.MemoryUtil.memPutInt; + +/** + * Reusable CPU-side mesh builder backed by native direct buffers. + * + *

Geometry is written between {@link #begin()} and {@link #end()}, then consumed through + * {@link #vertexBufferView()} and {@link #indexBufferView()} by the RHI dynamic-mesh upload path. + * The builder owns only its CPU buffers; GPU buffer allocation and lifetime are managed by the renderer.

+ * + *

ФАСАД (исключение Фазы 8): горячий путь записи атрибутов живёт здесь; рост буферов, + * debug-валидация и слияние мешей — в package-private хелперах `uniform/mesh/` + * (MeshBufferGrowth / MeshDebugChecks / MeshMerge, тот же package).

+ */ +public final class MeshBuilder implements AutoCloseable { + final VertexFormat format; + final int vertexStride; + final int primitiveIndicesCount; + private final double[] warped2 = new double[2]; + public double alpha = 1.0; + ByteBuffer vertices; + long verticesPtrStart, verticesPtr; + ByteBuffer indices; + long indicesPtr; + int vertexI; + int indicesCount; + boolean building; + private boolean worldCameraAnchored; + private double cameraX, cameraZ; + + public MeshBuilder(RenderPipeline pipeline) { + this(Objects.requireNonNull(pipeline.getVertexFormatBinding(0)), pipeline.getPrimitiveTopology()); + } + + public MeshBuilder(VertexFormat format, com.mojang.blaze3d.PrimitiveTopology drawMode) { + this.format = format; + this.vertexStride = format.getVertexSize(); + this.primitiveIndicesCount = drawMode.primitiveLength; + } + + public MeshBuilder(VertexFormat format, com.mojang.blaze3d.PrimitiveTopology drawMode, int vertexCount, int indexCount) { + this(format, drawMode); + reserve(vertexCount, indexCount); + } + + public void begin() { + if (RenderState.rendering3D) { + beginWorld(resolveRenderCameraPos()); + } else { + beginScreen(); + } + } + + /** + * Begin a 2D/screen-space mesh. No world camera anchoring is applied. + */ + public void beginScreen() { + beginInternal(false, Vec3.ZERO); + } + + /** + * Begin a world-space mesh with an explicit camera anchor. + * + *

World vertices keep full Y, but X/Z are written camera-relative to avoid + * float precision loss. The RHI world pass then applies the matching -cameraY + * translation immediately before writing the MeshData UBO. Do not make this + * depend on ambient RenderState in world command paths: the caller must pass + * the camera that belongs to the current frame.

+ */ + public void beginWorld(Vec3 cameraPos) { + if (cameraPos == null) { + throw new IllegalArgumentException("MeshBuilder.beginWorld() requires a camera position"); + } + beginInternal(true, cameraPos); + } + + private void beginInternal(boolean worldCameraAnchored, Vec3 cameraPos) { + if (building) throw new IllegalStateException("Mesh.begin() called while already building."); + + if (vertices == null || indices == null) { + reserve(MeshBufferGrowth.DEFAULT_VERTEX_CAPACITY, MeshBufferGrowth.DEFAULT_INDEX_CAPACITY); + } + + verticesPtr = verticesPtrStart; + vertexI = 0; + indicesCount = 0; + + this.worldCameraAnchored = worldCameraAnchored; + this.cameraX = worldCameraAnchored ? cameraPos.x : 0.0; + this.cameraZ = worldCameraAnchored ? cameraPos.z : 0.0; + + building = true; + } + + private static Vec3 resolveRenderCameraPos() { + if (RenderState.cameraPos != null && RenderState.cameraPos != Vec3.ZERO) { + return RenderState.cameraPos; + } + Minecraft mc = Minecraft.getInstance(); + if (mc != null && mc.gameRenderer != null && mc.gameRenderer.mainCamera() != null) { + return mc.gameRenderer.mainCamera().position(); + } + return Vec3.ZERO; + } + + public MeshBuilder vec3(double x, double y, double z) { + MeshDebugChecks.vertexWrite(this, 12, "Position3"); + + long p = verticesPtr; + + memPutFloat(p, (float) (x - cameraX)); + memPutFloat(p + 4, (float) y); + memPutFloat(p + 8, (float) (z - cameraZ)); + + verticesPtr += 12; + return this; + } + + public MeshBuilder vec2(double x, double y) { + MeshDebugChecks.vertexWrite(this, 8, "Position2"); + + RenderWarp warp = RenderWarpStack.current(); + if (warp.active()) { + warp.map(x, y, warped2); + x = warped2[0]; + y = warped2[1]; + } + + long p = verticesPtr; + + memPutFloat(p, (float) x); + memPutFloat(p + 4, (float) y); + + verticesPtr += 8; + return this; + } + + public MeshBuilder raw2(double x, double y) { + MeshDebugChecks.vertexWrite(this, 8, "Raw2"); + + long p = verticesPtr; + + memPutFloat(p, (float) x); + memPutFloat(p + 4, (float) y); + + verticesPtr += 8; + return this; + } + + public MeshBuilder local2(double x, double y) { + MeshDebugChecks.vertexWrite(this, 16, "Local"); + + RenderWarp warp = RenderWarpStack.current(); + double invW = warp.active() ? warp.interpolationInvW(x, y) : 1.0; + + long p = verticesPtr; + + memPutFloat(p, (float) (x * invW)); + memPutFloat(p + 4, (float) (y * invW)); + memPutFloat(p + 8, (float) invW); + memPutFloat(p + 12, 0.0f); + + verticesPtr += 16; + return this; + } + + public MeshBuilder vec4(float x, float y, float z, float w) { + MeshDebugChecks.vertexWrite(this, 16, "Vec4"); + + long p = verticesPtr; + + memPutFloat(p, x); + memPutFloat(p + 4, y); + memPutFloat(p + 8, z); + memPutFloat(p + 12, w); + + verticesPtr += 16; + return this; + } + + public MeshBuilder vec4(double x, double y, double z, double w) { + return vec4((float) x, (float) y, (float) z, (float) w); + } + + /** + * Color as bytes 0..255 + */ + public MeshBuilder color(int r, int g, int b, int a) { + MeshDebugChecks.vertexWrite(this, 4, "Color"); + + long p = verticesPtr; + + memPutByte(p, (byte) r); + memPutByte(p + 1, (byte) g); + memPutByte(p + 2, (byte) b); + memPutByte(p + 3, (byte) Math.max(0, Math.min(255, (int) (a * alpha)))); + + verticesPtr += 4; + return this; + } + + /** + * Color as floats 0..1 + */ + public MeshBuilder color(float r, float g, float b, float a) { + return color( + (int) (r * 255f), + (int) (g * 255f), + (int) (b * 255f), + (int) (a * 255f) + ); + } + + /** + * Color from RenderColor (0..255). + */ + public MeshBuilder color(RenderColor color) { + return color(color.r, color.g, color.b, color.a); + } + + public int next() { + MeshDebugChecks.completedVertex(this); + return vertexI++; + } + + public void line(int i1, int i2) { + MeshDebugChecks.indexWrite(this, 2, "line"); + + long p = indicesPtr + indicesCount * 4L; + + memPutInt(p, i1); + memPutInt(p + 4, i2); + + indicesCount += 2; + } + + public void triangle(int i1, int i2, int i3) { + MeshDebugChecks.indexWrite(this, 3, "triangle"); + + long p = indicesPtr + indicesCount * 4L; + + memPutInt(p, i1); + memPutInt(p + 4, i2); + memPutInt(p + 8, i3); + + indicesCount += 3; + } + + public void quad(int i1, int i2, int i3, int i4) { + MeshDebugChecks.indexWrite(this, 6, "quad"); + + long p = indicesPtr + indicesCount * 4L; + + memPutInt(p, i1); + memPutInt(p + 4, i2); + memPutInt(p + 8, i3); + + memPutInt(p + 12, i3); + memPutInt(p + 16, i4); + memPutInt(p + 20, i1); + + indicesCount += 6; + } + + public void ensureLineCapacity() { + ensureCapacity(2, 2); + } + + public void ensureTriCapacity() { + ensureCapacity(3, 3); + } + + public void ensureQuadCapacity() { + ensureCapacity(4, 6); + } + + public void ensureCapacity(int addVertices, int addIndices) { + MeshBufferGrowth.ensure(this, addVertices, addIndices); + } + + /** + * Ensures that the CPU-side direct buffers can hold at least the requested amount of vertices/indices. + * + *

This method is intentionally separate from {@link #begin()}: render code that owns a long-lived + * MeshBuilder can pre-warm it during initialization instead of paying ByteBuffer.allocateDirect on the + * render-thread hot path. If the current buffers are already large enough, this is a no-op.

+ */ + public void reserve(int vertexCount, int indexCount) { + MeshBufferGrowth.reserve(this, vertexCount, indexCount); + } + + public void end() { + validateComplete("MeshBuilder.end"); + building = false; + } + + public boolean isBuilding() { + return building; + } + + /** + * Append an already-built mesh into this builder, rebasing INT indices by the current vertex count. + * This is used by higher level batchers (UI/text) to preserve draw order without flushing per element. + */ + public void appendMesh(MeshBuilder source) { + MeshMerge.append(this, source); + } + + public ByteBuffer vertexBufferView() { + validateComplete("MeshBuilder.vertexBufferView"); + ByteBuffer copy = vertices.duplicate().order(ByteOrder.nativeOrder()); + copy.position(0); + copy.limit(getVerticesOffset()); + return copy.slice().order(ByteOrder.nativeOrder()); + } + + public ByteBuffer indexBufferView() { + validateComplete("MeshBuilder.indexBufferView"); + ByteBuffer copy = indices.duplicate().order(ByteOrder.nativeOrder()); + copy.position(0); + copy.limit(indicesCount * Integer.BYTES); + return copy.slice().order(ByteOrder.nativeOrder()); + } + + public int getVertexBytes() { + return getVerticesOffset(); + } + + public int getVertexStride() { + return vertexStride; + } + + public boolean isWorldCameraAnchored() { + return worldCameraAnchored; + } + + public double cameraAnchorX() { + return cameraX; + } + + public double cameraAnchorZ() { + return cameraZ; + } + + public int getIndexBytes() { + return indicesCount * Integer.BYTES; + } + + /** + * Emergency compatibility upload. This path is locked after the RHI migration and is not allowed as production + * behavior. Use LoVisualRHI.dynamicMeshes().upload(mesh) instead. + */ + @Deprecated + public GpuBuffer getVertexBuffer() { + RenderArchitectureGuard.requireAllowed(LegacyRenderPath.IMMEDIATE_MESH_UPLOAD, "MeshBuilder#getVertexBuffer"); + return RenderSystem.getDevice().createBuffer( + () -> "lovisual-immediate-vertices", + GpuBuffer.USAGE_VERTEX | GpuBuffer.USAGE_COPY_DST, + vertexBufferView() + ); + } + + /** + * Emergency compatibility upload. This path is locked after the RHI migration and is not allowed as production + * behavior. Use LoVisualRHI.dynamicMeshes().upload(mesh) instead. + */ + @Deprecated + public GpuBuffer getIndexBuffer() { + RenderArchitectureGuard.requireAllowed(LegacyRenderPath.IMMEDIATE_MESH_UPLOAD, "MeshBuilder#getIndexBuffer"); + return RenderSystem.getDevice().createBuffer( + () -> "lovisual-immediate-indices", + GpuBuffer.USAGE_INDEX | GpuBuffer.USAGE_COPY_DST, + indexBufferView() + ); + } + + public com.mojang.blaze3d.IndexType getIndexType() { + // we write ints in indices() + return com.mojang.blaze3d.IndexType.INT; + } + + public int getIndicesCount() { + return indicesCount; + } + + public int getVertexCount() { + return vertexI; + } + + int getVerticesOffset() { + return (int) (verticesPtr - verticesPtrStart); + } + + public void validateComplete(String owner) { + MeshDebugChecks.validateComplete(this, owner); + } + + /** + * Releases CPU-side native buffers owned by this builder. The builder may be reused later; + * the next begin()/reserve() call will allocate fresh buffers. + */ + @Override + public void close() { + if (building) { + building = false; + } + MeshBufferGrowth.freeDirect(vertices); + MeshBufferGrowth.freeDirect(indices); + vertices = null; + indices = null; + verticesPtrStart = 0L; + verticesPtr = 0L; + indicesPtr = 0L; + vertexI = 0; + indicesCount = 0; + worldCameraAnchored = false; + cameraX = 0.0; + cameraZ = 0.0; + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/MeshData.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/MeshData.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/MeshData.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/uniform/MeshData.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/esp/ShaderEspBlurUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/esp/ShaderEspBlurUniforms.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/esp/ShaderEspBlurUniforms.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/esp/ShaderEspBlurUniforms.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/esp/ShaderEspGradientUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/esp/ShaderEspGradientUniforms.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/esp/ShaderEspGradientUniforms.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/esp/ShaderEspGradientUniforms.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/esp/ShaderEspSmokeUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/esp/ShaderEspSmokeUniforms.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/esp/ShaderEspSmokeUniforms.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/esp/ShaderEspSmokeUniforms.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/HandBlendUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/HandBlendUniforms.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/HandBlendUniforms.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/HandBlendUniforms.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/HandChamsUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/HandChamsUniforms.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/HandChamsUniforms.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/HandChamsUniforms.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/HandGlassUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/HandGlassUniforms.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/HandGlassUniforms.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/HandGlassUniforms.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/HandMetallicUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/HandMetallicUniforms.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/HandMetallicUniforms.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/HandMetallicUniforms.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/effects/HandNoiseUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/effects/HandNoiseUniforms.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/effects/HandNoiseUniforms.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/effects/HandNoiseUniforms.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/effects/HandSmokeUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/effects/HandSmokeUniforms.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/effects/HandSmokeUniforms.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/effects/HandSmokeUniforms.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/effects/TextureTintUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/effects/TextureTintUniforms.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/effects/TextureTintUniforms.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/effects/TextureTintUniforms.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/post/DepthOfFieldUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/post/DepthOfFieldUniforms.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/post/DepthOfFieldUniforms.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/post/DepthOfFieldUniforms.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/post/MotionBlurUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/post/MotionBlurUniforms.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/post/MotionBlurUniforms.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/post/MotionBlurUniforms.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/post/PostFXUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/post/PostFXUniforms.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/post/PostFXUniforms.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/post/PostFXUniforms.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/post/PostProcessUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/post/PostProcessUniforms.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/post/PostProcessUniforms.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/post/PostProcessUniforms.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/postprocess/SkyShaderNebulaUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/postprocess/SkyShaderNebulaUniforms.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/postprocess/SkyShaderNebulaUniforms.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/postprocess/SkyShaderNebulaUniforms.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/postprocess/SkyShaderStarryUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/postprocess/SkyShaderStarryUniforms.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/postprocess/SkyShaderStarryUniforms.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/postprocess/SkyShaderStarryUniforms.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/postprocess/SkyShaderUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/postprocess/SkyShaderUniforms.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/postprocess/SkyShaderUniforms.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/postprocess/SkyShaderUniforms.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/skybox/ShaderSkyUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/skybox/ShaderSkyUniforms.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/skybox/ShaderSkyUniforms.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/skybox/ShaderSkyUniforms.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/skybox/SkyboxShaderUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/skybox/SkyboxShaderUniforms.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/skybox/SkyboxShaderUniforms.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/skybox/SkyboxShaderUniforms.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/sun/SkySunUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/sun/SkySunUniforms.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/sun/SkySunUniforms.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/sun/SkySunUniforms.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/ui/MenuBackgroundUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/ui/MenuBackgroundUniforms.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/ui/MenuBackgroundUniforms.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/ui/MenuBackgroundUniforms.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/ui/MsdfTextUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/ui/MsdfTextUniforms.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/ui/MsdfTextUniforms.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/ui/MsdfTextUniforms.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/ui/UIBatchUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/ui/UIBatchUniforms.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/ui/UIBatchUniforms.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/ui/UIBatchUniforms.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/ui/UIBlurUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/ui/UIBlurUniforms.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/ui/UIBlurUniforms.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/ui/UIBlurUniforms.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/HeatUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/HeatUniforms.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/HeatUniforms.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/HeatUniforms.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/KillEffectBeamUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/KillEffectBeamUniforms.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/KillEffectBeamUniforms.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/KillEffectBeamUniforms.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/MeshUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/MeshUniforms.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/MeshUniforms.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/MeshUniforms.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/liquid/WaterHighlightUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/liquid/WaterHighlightUniforms.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/liquid/WaterHighlightUniforms.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/liquid/WaterHighlightUniforms.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/liquid/WetSurfaceUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/liquid/WetSurfaceUniforms.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/liquid/WetSurfaceUniforms.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/liquid/WetSurfaceUniforms.java diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/mesh/MeshBufferGrowth.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/mesh/MeshBufferGrowth.java new file mode 100644 index 0000000..6ff82e8 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/mesh/MeshBufferGrowth.java @@ -0,0 +1,114 @@ +package dev.loki.lovisual.render.engine.uniform; + +import java.nio.ByteBuffer; + +import static org.lwjgl.system.MemoryUtil.memAddress0; +import static org.lwjgl.system.MemoryUtil.memAlloc; +import static org.lwjgl.system.MemoryUtil.memCopy; +import static org.lwjgl.system.MemoryUtil.memFree; + +/** + * Рост/аллокация CPU-буферов MeshBuilder (native direct ByteBuffer): + * копирование данных при реаллокации, дефолтные размеры, reserve. + */ +final class MeshBufferGrowth { + + static final int DEFAULT_VERTEX_CAPACITY = Integer.getInteger("lovisual.render.mesh.defaultVertices", 4096); + static final int DEFAULT_INDEX_CAPACITY = Integer.getInteger("lovisual.render.mesh.defaultIndices", 8192); + private static final int MAX_BUFFER_BYTES = Integer.MAX_VALUE - 8; + + private MeshBufferGrowth() {} + + static ByteBuffer allocateDirect(int bytes) { + if (bytes <= 0) { + throw new IllegalArgumentException("MeshBuilder buffer size must be positive: " + bytes); + } + if (bytes > MAX_BUFFER_BYTES) { + throw new OutOfMemoryError("MeshBuilder buffer request is too large: " + bytes); + } + return memAlloc(bytes); + } + + static void freeDirect(ByteBuffer buffer) { + if (buffer != null) { + memFree(buffer); + } + } + + static void ensure(MeshBuilder m, int addVertices, int addIndices) { + if (MeshDebugChecks.DEBUG && addIndices % m.primitiveIndicesCount != 0) { + throw new IllegalArgumentException("Unexpected amount of indices written to MeshBuilder."); + } + + if (m.vertices == null || m.indices == null) { + reserve(m, Math.max(DEFAULT_VERTEX_CAPACITY, m.vertexI + addVertices), + Math.max(DEFAULT_INDEX_CAPACITY, m.indicesCount + addIndices)); + return; + } + + int vNeedBytes = (m.vertexI + addVertices) * m.vertexStride; + if (vNeedBytes > m.vertices.capacity()) { + int offset = m.getVerticesOffset(); + int newSize = Math.max(m.vertices.capacity() * 2, m.vertices.capacity() + addVertices * m.vertexStride); + + ByteBuffer oldVertices = m.vertices; + ByteBuffer newVertices = allocateDirect(newSize); + memCopy(memAddress0(oldVertices), memAddress0(newVertices), offset); + + m.vertices = newVertices; + m.verticesPtrStart = memAddress0(m.vertices); + m.verticesPtr = m.verticesPtrStart + offset; + freeDirect(oldVertices); + } + + int iNeedBytes = (m.indicesCount + addIndices) * Integer.BYTES; + if (iNeedBytes > m.indices.capacity()) { + int newSize = Math.max(m.indices.capacity() * 2, m.indices.capacity() + addIndices * Integer.BYTES); + + ByteBuffer oldIndices = m.indices; + ByteBuffer newIndices = allocateDirect(newSize); + memCopy(memAddress0(oldIndices), memAddress0(newIndices), (long) m.indicesCount * 4L); + + m.indices = newIndices; + m.indicesPtr = memAddress0(m.indices); + freeDirect(oldIndices); + } + } + + /** + * Гарантирует, что CPU-буферы вмещают как минимум запрошенное число + * вершин/индексов. Отдельно от begin(): долгоживущие билдеры можно + * прогревать на инициализации, а не в горячем пути. + */ + static void reserve(MeshBuilder m, int vertexCount, int indexCount) { + int vertexBytes = m.vertexStride * Math.max(1, vertexCount); + int indexBytes = Math.max(1, indexCount) * Integer.BYTES; + + if (m.vertices == null || vertexBytes > m.vertices.capacity()) { + int offset = m.vertices != null ? m.getVerticesOffset() : 0; + ByteBuffer oldVertices = m.vertices; + ByteBuffer newVertices = allocateDirect(vertexBytes); + if (oldVertices != null && offset > 0) { + memCopy(memAddress0(oldVertices), memAddress0(newVertices), offset); + } + + m.vertices = newVertices; + m.verticesPtrStart = memAddress0(m.vertices); + m.verticesPtr = m.verticesPtrStart + offset; + freeDirect(oldVertices); + } + + if (m.indices == null || indexBytes > m.indices.capacity()) { + long offset = m.indices != null ? (long) m.indicesCount * Integer.BYTES : 0L; + ByteBuffer oldIndices = m.indices; + ByteBuffer newIndices = allocateDirect(indexBytes); + if (oldIndices != null && offset > 0L) { + memCopy(memAddress0(oldIndices), memAddress0(newIndices), offset); + } + + m.indices = newIndices; + m.indicesPtr = memAddress0(m.indices); + freeDirect(oldIndices); + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/mesh/MeshDebugChecks.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/mesh/MeshDebugChecks.java new file mode 100644 index 0000000..07754c6 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/mesh/MeshDebugChecks.java @@ -0,0 +1,140 @@ +package dev.loki.lovisual.render.engine.uniform; + +import net.fabricmc.loader.api.FabricLoader; + +import static org.lwjgl.system.MemoryUtil.memGetInt; + +/** + * Debug-валидация MeshBuilder: контроль stride/индексов/буферов. + * Вся логика гейтится константой {@link #DEBUG} и в production + * вычищается JIT (dead code elimination). + */ +final class MeshDebugChecks { + + static final boolean DEBUG = + FabricLoader.getInstance().isDevelopmentEnvironment() || Boolean.getBoolean("lovisual.render.debug"); + + private MeshDebugChecks() {} + + static void hasBuffers(MeshBuilder m, String owner) { + if (m.vertices == null || m.indices == null) { + throw new IllegalStateException(owner + ": MeshBuilder has no CPU buffers; call begin()/reserve() before writing."); + } + } + + static void vertexWrite(MeshBuilder m, int bytes, String attribute) { + if (!DEBUG) return; + hasBuffers(m, "MeshBuilder." + attribute); + if (!m.building) { + throw new IllegalStateException("MeshBuilder." + attribute + " write outside begin()/end(): format=" + m.format); + } + if (bytes <= 0) { + throw new IllegalArgumentException("MeshBuilder." + attribute + " invalid write size: " + bytes); + } + int vertexBytes = m.getVerticesOffset(); + int currentVertexBytes = vertexBytes - m.vertexI * m.vertexStride; + if (currentVertexBytes < 0 || currentVertexBytes > m.vertexStride) { + throw new IllegalStateException("MeshBuilder." + attribute + " cursor is not inside current vertex: vertex=" + m.vertexI + + ", cursorBytes=" + vertexBytes + + ", currentVertexBytes=" + currentVertexBytes + + ", stride=" + m.vertexStride + + ", format=" + m.format); + } + if (currentVertexBytes + bytes > m.vertexStride) { + throw new IllegalStateException("MeshBuilder." + attribute + " overruns vertex stride: vertex=" + m.vertexI + + ", currentVertexBytes=" + currentVertexBytes + + ", writeBytes=" + bytes + + ", stride=" + m.vertexStride + + ", format=" + m.format); + } + if (vertexBytes + bytes > m.vertices.capacity()) { + throw new IndexOutOfBoundsException("MeshBuilder." + attribute + " write exceeds vertex CPU buffer: vertex=" + m.vertexI + + ", cursorBytes=" + vertexBytes + + ", writeBytes=" + bytes + + ", capacity=" + m.vertices.capacity() + + ", format=" + m.format); + } + } + + static void completedVertex(MeshBuilder m) { + if (!DEBUG) return; + hasBuffers(m, "MeshBuilder.next"); + int vertexBytes = m.getVerticesOffset(); + int expected = (m.vertexI + 1) * m.vertexStride; + if (vertexBytes != expected) { + throw new IllegalStateException("MeshBuilder.next() called with incomplete/overwritten vertex: vertex=" + m.vertexI + + ", expectedBytes=" + expected + + ", actualBytes=" + vertexBytes + + ", stride=" + m.vertexStride + + ", format=" + m.format); + } + } + + static void indexWrite(MeshBuilder m, int addIndices, String primitive) { + if (!DEBUG) return; + hasBuffers(m, "MeshBuilder." + primitive); + if (!m.building) { + throw new IllegalStateException("MeshBuilder." + primitive + " index write outside begin()/end(): format=" + m.format); + } + if (addIndices <= 0) { + throw new IllegalArgumentException("MeshBuilder." + primitive + " invalid index count: " + addIndices); + } + int indexBytes = m.indicesCount * Integer.BYTES; + int writeBytes = addIndices * Integer.BYTES; + if (indexBytes + writeBytes > m.indices.capacity()) { + throw new IndexOutOfBoundsException("MeshBuilder." + primitive + " write exceeds index CPU buffer: indices=" + m.indicesCount + + ", add=" + addIndices + + ", capacityBytes=" + m.indices.capacity() + + ", format=" + m.format); + } + } + + static void validateIndices(MeshBuilder m, String owner) { + if (!DEBUG || m.indicesCount <= 0) return; + long p = m.indicesPtr; + for (int i = 0; i < m.indicesCount; i++) { + int index = memGetInt(p + (long) i * Integer.BYTES); + if (index < 0 || index >= m.vertexI) { + throw new IndexOutOfBoundsException(owner + ": MeshBuilder index references missing vertex: indexOrdinal=" + i + + ", index=" + index + + ", vertexCount=" + m.vertexI + + ", indicesCount=" + m.indicesCount + + ", format=" + m.format); + } + } + } + + static void validateComplete(MeshBuilder m, String owner) { + hasBuffers(m, owner); + int vertexBytes = m.getVerticesOffset(); + int expectedVertexBytes = m.vertexI * m.vertexStride; + if (vertexBytes != expectedVertexBytes) { + throw new IllegalStateException(owner + ": MeshBuilder vertex stride mismatch: vertexCount=" + m.vertexI + + ", expectedVertexBytes=" + expectedVertexBytes + + ", actualVertexBytes=" + vertexBytes + + ", stride=" + m.vertexStride + + ", format=" + m.format); + } + if (vertexBytes < 0 || vertexBytes > m.vertices.capacity()) { + throw new IndexOutOfBoundsException(owner + ": MeshBuilder vertex range outside CPU buffer: bytes=" + + vertexBytes + ", capacity=" + m.vertices.capacity() + ", format=" + m.format); + } + int indexBytes = m.indicesCount * Integer.BYTES; + if (indexBytes < 0 || indexBytes > m.indices.capacity()) { + throw new IndexOutOfBoundsException(owner + ": MeshBuilder index range outside CPU buffer: bytes=" + + indexBytes + ", capacity=" + m.indices.capacity() + ", format=" + m.format); + } + if (m.primitiveIndicesCount > 0 && m.indicesCount % m.primitiveIndicesCount != 0) { + if (DEBUG) { + throw new IllegalStateException(owner + ": MeshBuilder incomplete primitive payload: indices=" + m.indicesCount + + ", primitiveLength=" + m.primitiveIndicesCount + ", format=" + m.format); + } + int trimmed = (m.indicesCount / m.primitiveIndicesCount) * m.primitiveIndicesCount; + // Trim incomplete tail to avoid Render Frame crash (e.g. 176 % 3 == 2 on TRIANGLES). + org.slf4j.LoggerFactory.getLogger(MeshBuilder.class).warn("{}: trimming incomplete primitive payload: indices={} -> {} (primitiveLength={}, format={})", owner, m.indicesCount, trimmed, m.primitiveIndicesCount, m.format); + m.indicesCount = trimmed; + if (m.indicesCount == 0) return; + } + validateIndices(m, owner); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/mesh/MeshMerge.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/mesh/MeshMerge.java new file mode 100644 index 0000000..ff28d9f --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/mesh/MeshMerge.java @@ -0,0 +1,47 @@ +package dev.loki.lovisual.render.engine.uniform; + +import java.nio.ByteBuffer; + +import static org.lwjgl.system.MemoryUtil.memAddress0; +import static org.lwjgl.system.MemoryUtil.memCopy; +import static org.lwjgl.system.MemoryUtil.memGetInt; +import static org.lwjgl.system.MemoryUtil.memPutInt; + +/** + * Слияние собранных мешей: appendMesh с ребазой INT-индексов на текущее + * число вершин приёмника (порядок отрисовки сохраняется без flush). + */ +final class MeshMerge { + + private MeshMerge() {} + + static void append(MeshBuilder dst, MeshBuilder source) { + if (source == null || source.getVertexCount() <= 0 || source.getIndicesCount() <= 0) return; + if (source.isBuilding()) source.end(); + if (!dst.building) { + throw new IllegalStateException("appendMesh() requires destination mesh to be building."); + } + if (dst.vertexStride != source.vertexStride || !dst.format.equals(source.format)) { + throw new IllegalArgumentException("Cannot append mesh with different vertex format/stride."); + } + + int sourceVertices = source.getVertexCount(); + int sourceIndices = source.getIndicesCount(); + int baseVertex = dst.vertexI; + dst.ensureCapacity(sourceVertices, sourceIndices); + + ByteBuffer vertexSource = source.vertexBufferView(); + long vertexBytes = source.getVertexBytes(); + memCopy(memAddress0(vertexSource), dst.verticesPtr, vertexBytes); + dst.verticesPtr += vertexBytes; + dst.vertexI += sourceVertices; + + ByteBuffer indexSource = source.indexBufferView(); + long indexIn = memAddress0(indexSource); + long indexOut = dst.indicesPtr + (long) dst.indicesCount * Integer.BYTES; + for (int i = 0; i < sourceIndices; i++) { + memPutInt(indexOut + (long) i * Integer.BYTES, memGetInt(indexIn + (long) i * Integer.BYTES) + baseVertex); + } + dst.indicesCount += sourceIndices; + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/storage/FixedUniformStorage.java b/mod/src/main/java/dev/loki/lovisual/render/engine/uniform/storage/FixedUniformStorage.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/storage/FixedUniformStorage.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/uniform/storage/FixedUniformStorage.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/vertex/LoVisualVertexFormatElements.java b/mod/src/main/java/dev/loki/lovisual/render/engine/vertex/LoVisualVertexFormatElements.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/vertex/LoVisualVertexFormatElements.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/vertex/LoVisualVertexFormatElements.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/vertex/LoVisualVertexFormats.java b/mod/src/main/java/dev/loki/lovisual/render/engine/vertex/LoVisualVertexFormats.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/vertex/LoVisualVertexFormats.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/vertex/LoVisualVertexFormats.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/visuals/LoVisualVisualFrame.java b/mod/src/main/java/dev/loki/lovisual/render/engine/visuals/LoVisualVisualFrame.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/visuals/LoVisualVisualFrame.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/visuals/LoVisualVisualFrame.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/visuals/LoVisualVisualPass.java b/mod/src/main/java/dev/loki/lovisual/render/engine/visuals/LoVisualVisualPass.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/visuals/LoVisualVisualPass.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/visuals/LoVisualVisualPass.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/visuals/LoVisualVisualPhase.java b/mod/src/main/java/dev/loki/lovisual/render/engine/visuals/LoVisualVisualPhase.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/visuals/LoVisualVisualPhase.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/visuals/LoVisualVisualPhase.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/visuals/LoVisualVisuals.java b/mod/src/main/java/dev/loki/lovisual/render/engine/visuals/LoVisualVisuals.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/visuals/LoVisualVisuals.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/visuals/LoVisualVisuals.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/world/WorldBatcher.java b/mod/src/main/java/dev/loki/lovisual/render/engine/world/WorldBatcher.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/world/WorldBatcher.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/world/WorldBatcher.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/world/WorldCommandBuffer.java b/mod/src/main/java/dev/loki/lovisual/render/engine/world/WorldCommandBuffer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/world/WorldCommandBuffer.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/world/WorldCommandBuffer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/world/WorldDrawCommand.java b/mod/src/main/java/dev/loki/lovisual/render/engine/world/WorldDrawCommand.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/world/WorldDrawCommand.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/world/WorldDrawCommand.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/world/WorldPassCompiler.java b/mod/src/main/java/dev/loki/lovisual/render/engine/world/WorldPassCompiler.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/world/WorldPassCompiler.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/world/WorldPassCompiler.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/world/mesh/WorldWideLineMeshCompiler.java b/mod/src/main/java/dev/loki/lovisual/render/engine/world/mesh/WorldWideLineMeshCompiler.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/world/mesh/WorldWideLineMeshCompiler.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/world/mesh/WorldWideLineMeshCompiler.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/world/stats/WorldRenderStats.java b/mod/src/main/java/dev/loki/lovisual/render/engine/world/stats/WorldRenderStats.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/world/stats/WorldRenderStats.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/world/stats/WorldRenderStats.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/world/stats/WorldRenderStatsSnapshot.java b/mod/src/main/java/dev/loki/lovisual/render/engine/world/stats/WorldRenderStatsSnapshot.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/world/stats/WorldRenderStatsSnapshot.java rename to mod/src/main/java/dev/loki/lovisual/render/engine/world/stats/WorldRenderStatsSnapshot.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/fade/ViewObstructionFadeContext.java b/mod/src/main/java/dev/loki/lovisual/render/fade/ViewObstructionFadeContext.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/fade/ViewObstructionFadeContext.java rename to mod/src/main/java/dev/loki/lovisual/render/fade/ViewObstructionFadeContext.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/fade/ViewObstructionFadeState.java b/mod/src/main/java/dev/loki/lovisual/render/fade/ViewObstructionFadeState.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/fade/ViewObstructionFadeState.java rename to mod/src/main/java/dev/loki/lovisual/render/fade/ViewObstructionFadeState.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/helpers/clip/ClipFunction.java b/mod/src/main/java/dev/loki/lovisual/render/helpers/clip/ClipFunction.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/helpers/clip/ClipFunction.java rename to mod/src/main/java/dev/loki/lovisual/render/helpers/clip/ClipFunction.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/helpers/clip/ScissorFunction.java b/mod/src/main/java/dev/loki/lovisual/render/helpers/clip/ScissorFunction.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/helpers/clip/ScissorFunction.java rename to mod/src/main/java/dev/loki/lovisual/render/helpers/clip/ScissorFunction.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/helpers/clip/ScreenProjection.java b/mod/src/main/java/dev/loki/lovisual/render/helpers/clip/ScreenProjection.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/helpers/clip/ScreenProjection.java rename to mod/src/main/java/dev/loki/lovisual/render/helpers/clip/ScreenProjection.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/helpers/clip/ScreenSpaceOverlay2D.java b/mod/src/main/java/dev/loki/lovisual/render/helpers/clip/ScreenSpaceOverlay2D.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/helpers/clip/ScreenSpaceOverlay2D.java rename to mod/src/main/java/dev/loki/lovisual/render/helpers/clip/ScreenSpaceOverlay2D.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/helpers/material/EffectIconRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/helpers/material/EffectIconRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/helpers/material/EffectIconRenderer.java rename to mod/src/main/java/dev/loki/lovisual/render/helpers/material/EffectIconRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/helpers/material/MatteHudStyle.java b/mod/src/main/java/dev/loki/lovisual/render/helpers/material/MatteHudStyle.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/helpers/material/MatteHudStyle.java rename to mod/src/main/java/dev/loki/lovisual/render/helpers/material/MatteHudStyle.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/helpers/material/SodiumMaterialFlags.java b/mod/src/main/java/dev/loki/lovisual/render/helpers/material/SodiumMaterialFlags.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/helpers/material/SodiumMaterialFlags.java rename to mod/src/main/java/dev/loki/lovisual/render/helpers/material/SodiumMaterialFlags.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/helpers/material/SodiumSurfaceFlagContext.java b/mod/src/main/java/dev/loki/lovisual/render/helpers/material/SodiumSurfaceFlagContext.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/helpers/material/SodiumSurfaceFlagContext.java rename to mod/src/main/java/dev/loki/lovisual/render/helpers/material/SodiumSurfaceFlagContext.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/helpers/particle/OrientedQuadRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/helpers/particle/OrientedQuadRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/helpers/particle/OrientedQuadRenderer.java rename to mod/src/main/java/dev/loki/lovisual/render/helpers/particle/OrientedQuadRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/helpers/player/HeadAnchor.java b/mod/src/main/java/dev/loki/lovisual/render/helpers/player/HeadAnchor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/helpers/player/HeadAnchor.java rename to mod/src/main/java/dev/loki/lovisual/render/helpers/player/HeadAnchor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/helpers/player/Particle3D.java b/mod/src/main/java/dev/loki/lovisual/render/helpers/player/Particle3D.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/helpers/player/Particle3D.java rename to mod/src/main/java/dev/loki/lovisual/render/helpers/player/Particle3D.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/helpers/player/PlayerHeadRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/helpers/player/PlayerHeadRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/helpers/player/PlayerHeadRenderer.java rename to mod/src/main/java/dev/loki/lovisual/render/helpers/player/PlayerHeadRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/helpers/player/TrailPoint.java b/mod/src/main/java/dev/loki/lovisual/render/helpers/player/TrailPoint.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/helpers/player/TrailPoint.java rename to mod/src/main/java/dev/loki/lovisual/render/helpers/player/TrailPoint.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/helpers/util/GuiSpriteBatch.java b/mod/src/main/java/dev/loki/lovisual/render/helpers/util/GuiSpriteBatch.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/helpers/util/GuiSpriteBatch.java rename to mod/src/main/java/dev/loki/lovisual/render/helpers/util/GuiSpriteBatch.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/helpers/util/ParticleTextureMode.java b/mod/src/main/java/dev/loki/lovisual/render/helpers/util/ParticleTextureMode.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/helpers/util/ParticleTextureMode.java rename to mod/src/main/java/dev/loki/lovisual/render/helpers/util/ParticleTextureMode.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/helpers/util/SystemCursor.java b/mod/src/main/java/dev/loki/lovisual/render/helpers/util/SystemCursor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/helpers/util/SystemCursor.java rename to mod/src/main/java/dev/loki/lovisual/render/helpers/util/SystemCursor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/helpers/util/TickDelta.java b/mod/src/main/java/dev/loki/lovisual/render/helpers/util/TickDelta.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/helpers/util/TickDelta.java rename to mod/src/main/java/dev/loki/lovisual/render/helpers/util/TickDelta.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/invisible/SeeInvisiblesReveal.java b/mod/src/main/java/dev/loki/lovisual/render/invisible/SeeInvisiblesReveal.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/invisible/SeeInvisiblesReveal.java rename to mod/src/main/java/dev/loki/lovisual/render/invisible/SeeInvisiblesReveal.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/invisible/SeeInvisiblesState.java b/mod/src/main/java/dev/loki/lovisual/render/invisible/SeeInvisiblesState.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/invisible/SeeInvisiblesState.java rename to mod/src/main/java/dev/loki/lovisual/render/invisible/SeeInvisiblesState.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/iris/compat/IrisCompatibilityFeature.java b/mod/src/main/java/dev/loki/lovisual/render/iris/compat/IrisCompatibilityFeature.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/iris/compat/IrisCompatibilityFeature.java rename to mod/src/main/java/dev/loki/lovisual/render/iris/compat/IrisCompatibilityFeature.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/iris/compat/IrisCompatibilityGuards.java b/mod/src/main/java/dev/loki/lovisual/render/iris/compat/IrisCompatibilityGuards.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/iris/compat/IrisCompatibilityGuards.java rename to mod/src/main/java/dev/loki/lovisual/render/iris/compat/IrisCompatibilityGuards.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/iris/compat/IrisCompatibilityProfile.java b/mod/src/main/java/dev/loki/lovisual/render/iris/compat/IrisCompatibilityProfile.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/iris/compat/IrisCompatibilityProfile.java rename to mod/src/main/java/dev/loki/lovisual/render/iris/compat/IrisCompatibilityProfile.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/iris/compat/IrisCompatibilityProfiles.java b/mod/src/main/java/dev/loki/lovisual/render/iris/compat/IrisCompatibilityProfiles.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/iris/compat/IrisCompatibilityProfiles.java rename to mod/src/main/java/dev/loki/lovisual/render/iris/compat/IrisCompatibilityProfiles.java diff --git a/mod/src/main/java/dev/loki/lovisual/render/iris/patch/ShaderPatchCompiler.java b/mod/src/main/java/dev/loki/lovisual/render/iris/patch/ShaderPatchCompiler.java new file mode 100644 index 0000000..e31d28c --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/iris/patch/ShaderPatchCompiler.java @@ -0,0 +1,66 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ +package dev.loki.lovisual.render.iris.patch; +import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.io.Reader; +import java.util.List; +import java.util.Locale; +import java.util.Set; +public enum ShaderPatchCompiler { + ; + public static PatchProgram parse(Reader source) throws IOException, CompileException { + return PatchParser.parse(source); + } + public static CompiledPatch compile(PatchProgram program, ImmutableList source) throws CompileException { + return compile(program, source, CompileMode.PREFLIGHT); + } + public static CompiledPatch compile(PatchProgram program, ImmutableList source, CompileMode mode) throws CompileException { + return compile(program, source, mode, Set.of()); + } + public static CompiledPatch compile(PatchProgram program, ImmutableList source, CompileMode mode, Set stages) throws CompileException { + return PatchCompileHelper.compile(program, source, mode, stages); + } + public static ImmutableList apply(CompiledPatch patch, ImmutableList source) { + return PatchCompileHelper.apply(patch, source); + } + public static String signature(String sourceLine) { + return PatchCompileHelper.signature(sourceLine); + } + public enum CompileMode { PREFLIGHT, APPLICATION } + public enum ShaderStage { + ANY, FRAGMENT, VERTEX, GEOMETRY, COMPUTE; + public static ShaderStage parse(String value) throws CompileException { + if (value == null || value.isBlank()) return ANY; + try { return ShaderStage.valueOf(value.trim().toUpperCase(Locale.ROOT)); } + catch (IllegalArgumentException e) { throw new CompileException("Unsupported shader stage '" + value + "'"); } + } + } + public enum ProbeType { + REQUIRE_HASH(true, "hash"), FORBID_HASH(false, "hash"), + REQUIRE_IDENT(true, "ident"), FORBID_IDENT(false, "ident"), + REQUIRE_FUNCTION(true, "function"), FORBID_FUNCTION(false, "function"), + REQUIRE_CALL(true, "call"), FORBID_CALL(false, "call"), + REQUIRE_UNIFORM(true, "uniform"), FORBID_UNIFORM(false, "uniform"), + REQUIRE_DEFINE(true, "define"), FORBID_DEFINE(false, "define"); + private final boolean required; private final String label; + ProbeType(boolean required, String label) { this.required = required; this.label = label; } + boolean required() { return required; } + String label() { return label; } + } + public enum SelectorKind { HASH, IDENT, FUNCTION, FUNCTION_OPEN, DEFINE, DRAWBUFFERS, STAGE_BEGIN, ZONE } + public enum OperationType { INSERT_BEFORE, INSERT_AFTER, REPLACE_LINE, REMOVE_LINE, REPLACE_RANGE, REPLACE_BETWEEN } + public record PatchProgram(int schemaVersion, List probes, List operations) { } + public record CompiledPatch(List edits) { } + public record Edit(int start, int end, List payload) { } + public record Probe(ProbeType type, String value) { } + public record Selector(SelectorKind kind, String value, int occurrence, boolean requireUnique) { } + public record Operation(OperationType type, Selector anchor, Selector end, boolean includeEnd, List payload) { } + public static final class CompileException extends Exception { + public CompileException(String message) { super(message); } + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/iris/patch/ShaderPatchCompilerCli.java b/mod/src/main/java/dev/loki/lovisual/render/iris/patch/ShaderPatchCompilerCli.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/iris/patch/ShaderPatchCompilerCli.java rename to mod/src/main/java/dev/loki/lovisual/render/iris/patch/ShaderPatchCompilerCli.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/iris/patch/ShaderPatchEngine.java b/mod/src/main/java/dev/loki/lovisual/render/iris/patch/ShaderPatchEngine.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/iris/patch/ShaderPatchEngine.java rename to mod/src/main/java/dev/loki/lovisual/render/iris/patch/ShaderPatchEngine.java diff --git a/mod/src/main/java/dev/loki/lovisual/render/iris/patch/compile/PatchCompileHelper.java b/mod/src/main/java/dev/loki/lovisual/render/iris/patch/compile/PatchCompileHelper.java new file mode 100644 index 0000000..6afb426 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/iris/patch/compile/PatchCompileHelper.java @@ -0,0 +1,146 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ +package dev.loki.lovisual.render.iris.patch; +import com.google.common.collect.ImmutableList; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.Set; +final class PatchCompileHelper { + private PatchCompileHelper() {} + static ShaderPatchCompiler.CompiledPatch compile(ShaderPatchCompiler.PatchProgram program, ImmutableList source, ShaderPatchCompiler.CompileMode mode, Set stages) throws ShaderPatchCompiler.CompileException { + if (mode == ShaderPatchCompiler.CompileMode.PREFLIGHT && !containsLoVisualPayload(source)) validateNoSourceExcerpt(program, source); + PatchSourceHelper.SourceIndex index = PatchSourceHelper.SourceIndex.create(source); + PatchSourceHelper.validateProbes(program, index); + ArrayList edits = new ArrayList<>(program.operations().size()); + for (ShaderPatchCompiler.Operation operation : program.operations()) { + int anchor = PatchSourceHelper.find(index, operation.anchor()); + switch (operation.type()) { + case INSERT_BEFORE -> edits.add(new ShaderPatchCompiler.Edit(anchor, anchor, operation.payload())); + case INSERT_AFTER -> edits.add(new ShaderPatchCompiler.Edit(anchor + 1, anchor + 1, operation.payload())); + case REPLACE_LINE -> edits.add(new ShaderPatchCompiler.Edit(anchor, anchor + 1, operation.payload())); + case REMOVE_LINE -> edits.add(new ShaderPatchCompiler.Edit(anchor, anchor + 1, List.of())); + case REPLACE_RANGE -> { + int end = PatchSourceHelper.find(index, operation.end()); + if (end < anchor) throw new ShaderPatchCompiler.CompileException("Range end precedes start"); + edits.add(new ShaderPatchCompiler.Edit(anchor, operation.includeEnd() ? end + 1 : end, operation.payload())); + } + case REPLACE_BETWEEN -> { + int end = PatchSourceHelper.find(index, operation.end()); + if (end <= anchor) throw new ShaderPatchCompiler.CompileException("Range end does not follow start"); + edits.add(new ShaderPatchCompiler.Edit(anchor + 1, end, operation.payload())); + } + } + } + validateNonOverlapping(edits); + validateStageBounds(edits, source, stages); + validateDelimiterDelta(edits, source); + edits.sort(Comparator.comparingInt(ShaderPatchCompiler.Edit::start).thenComparingInt(ShaderPatchCompiler.Edit::end).reversed()); + return new ShaderPatchCompiler.CompiledPatch(List.copyOf(edits)); + } + static ImmutableList apply(ShaderPatchCompiler.CompiledPatch patch, ImmutableList source) { + ArrayList output = new ArrayList<>(source); + for (ShaderPatchCompiler.Edit edit : patch.edits()) { + output.subList(edit.start(), edit.end()).clear(); + output.addAll(edit.start(), edit.payload()); + } + return ImmutableList.copyOf(output); + } + static String signature(String sourceLine) { + String normalized = normalizeLine(sourceLine); + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + return java.util.HexFormat.of().formatHex(digest.digest(normalized.getBytes(java.nio.charset.StandardCharsets.UTF_8))); + } catch (NoSuchAlgorithmException e) { throw new IllegalStateException("SHA-256 unavailable", e); } + } + static boolean containsLoVisualPayload(List source) { + for (String line : source) if (line.contains("lovisual") || line.contains("LOVISUAL_")) return true; + return false; + } + static void validateNoSourceExcerpt(ShaderPatchCompiler.PatchProgram program, List source) throws ShaderPatchCompiler.CompileException { + List normalizedSource = source.stream().map(PatchCompileHelper::normalizeLine).toList(); + for (ShaderPatchCompiler.Operation operation : program.operations()) { + List payload = operation.payload().stream().map(PatchCompileHelper::normalizeLine).toList(); + for (int payloadStart = 0; payloadStart + 2 < payload.size(); payloadStart++) { + if (payload.get(payloadStart).isEmpty() || payload.get(payloadStart + 1).isEmpty() || payload.get(payloadStart + 2).isEmpty()) continue; + for (int sourceStart = 0; sourceStart + 2 < normalizedSource.size(); sourceStart++) { + if (payload.get(payloadStart).equals(normalizedSource.get(sourceStart)) && payload.get(payloadStart + 1).equals(normalizedSource.get(sourceStart + 1)) && payload.get(payloadStart + 2).equals(normalizedSource.get(sourceStart + 2))) { + throw new ShaderPatchCompiler.CompileException("Patch payload reproduces a target source excerpt"); + } + } + } + } + } + static String normalizeLine(String sourceLine) { + String source = sourceLine == null ? "" : sourceLine; + StringBuilder normalized = new StringBuilder(source.length()); + for (int i = 0; i < source.length(); i++) { char c = source.charAt(i); if (!Character.isWhitespace(c)) normalized.append(c); } + return normalized.toString(); + } + static void validateNonOverlapping(List edits) throws ShaderPatchCompiler.CompileException { + ArrayList sorted = new ArrayList<>(edits); + sorted.sort(Comparator.comparingInt(ShaderPatchCompiler.Edit::start).thenComparingInt(ShaderPatchCompiler.Edit::end)); + int previousEnd = -1; + for (ShaderPatchCompiler.Edit edit : sorted) { + if (edit.start() < previousEnd) throw new ShaderPatchCompiler.CompileException("Patch operations overlap"); + previousEnd = Math.max(previousEnd, edit.end()); + } + } + static void validateStageBounds(List edits, List source, Set stages) throws ShaderPatchCompiler.CompileException { + if (stages == null || stages.isEmpty() || stages.contains(ShaderPatchCompiler.ShaderStage.ANY) || !sourceHasStageBlocks(source)) return; + for (ShaderPatchCompiler.Edit edit : edits) { + ShaderPatchCompiler.ShaderStage stage = stageBeforeLine(source, edit.start()); + if (stage == null || !stages.contains(stage)) throw new ShaderPatchCompiler.CompileException("Patch edit escapes allowed shader stage(s): editLine=" + (edit.start() + 1) + " stage=" + (stage == null ? "none" : stage.name().toLowerCase(Locale.ROOT)) + " allowed=" + stages); + } + } + static boolean sourceHasStageBlocks(List source) { + for (String line : source) { + String stripped = PatchSourceHelper.stripLineComment(line).trim(); + if (stripped.contains("FRAGMENT_SHADER") || stripped.contains("VERTEX_SHADER") || stripped.contains("GEOMETRY_SHADER") || stripped.contains("COMPUTE_SHADER")) return true; + } + return false; + } + static ShaderPatchCompiler.ShaderStage stageBeforeLine(List source, int lineIndex) { + ArrayList stack = new ArrayList<>(); + int end = Math.min(Math.max(lineIndex, 0), source.size()); + for (int i = 0; i < end; i++) { + String stripped = PatchSourceHelper.stripLineComment(source.get(i)).trim(); + ShaderPatchCompiler.ShaderStage begin = stageFromDirective(stripped); + if (begin != null) stack.add(begin); + else if (stripped.startsWith("#endif") && !stack.isEmpty()) stack.remove(stack.size() - 1); + } + for (int i = stack.size() - 1; i >= 0; i--) { ShaderPatchCompiler.ShaderStage stage = stack.get(i); if (stage != ShaderPatchCompiler.ShaderStage.ANY) return stage; } + return null; + } + static ShaderPatchCompiler.ShaderStage stageFromDirective(String stripped) { + if (!stripped.startsWith("#if")) return null; + if (stripped.contains("FRAGMENT_SHADER")) return ShaderPatchCompiler.ShaderStage.FRAGMENT; + if (stripped.contains("VERTEX_SHADER")) return ShaderPatchCompiler.ShaderStage.VERTEX; + if (stripped.contains("GEOMETRY_SHADER")) return ShaderPatchCompiler.ShaderStage.GEOMETRY; + if (stripped.contains("COMPUTE_SHADER")) return ShaderPatchCompiler.ShaderStage.COMPUTE; + return null; + } + static void validateDelimiterDelta(List edits, List source) throws ShaderPatchCompiler.CompileException { + for (char open : new char[]{'{', '(', '['}) { + char close = switch (open) { case '{' -> '}'; case '(' -> ')'; case '[' -> ']'; default -> throw new IllegalStateException(); }; + int delta = 0; + for (ShaderPatchCompiler.Edit edit : edits) { + delta += delimiterBalance(edit.payload(), open, close); + delta -= delimiterBalance(source.subList(edit.start(), edit.end()), open, close); + } + if (delta != 0) throw new ShaderPatchCompiler.CompileException("Patch changes " + open + close + " delimiter balance"); + } + } + static int delimiterBalance(List lines, char open, char close) { + int balance = 0; + for (String line : lines) for (int i = 0; i < line.length(); i++) { char c = line.charAt(i); if (c == open) balance++; if (c == close) balance--; } + return balance; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/iris/patch/compile/PatchSourceHelper.java b/mod/src/main/java/dev/loki/lovisual/render/iris/patch/compile/PatchSourceHelper.java new file mode 100644 index 0000000..d73f578 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/iris/patch/compile/PatchSourceHelper.java @@ -0,0 +1,174 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ +package dev.loki.lovisual.render.iris.patch; +import com.google.common.collect.ImmutableList; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.regex.Pattern; +final class PatchSourceHelper { + static final Pattern GLOBAL_DECLARATION = Pattern.compile("^(?:flat\\s+|smooth\\s+|noperspective\\s+|centroid\\s+)*(?:in|out|uniform|varying)\\b.*;"); + private PatchSourceHelper() {} + static void validateProbes(ShaderPatchCompiler.PatchProgram program, SourceIndex index) throws ShaderPatchCompiler.CompileException { + ArrayList missing = new ArrayList<>(); + ArrayList forbidden = new ArrayList<>(); + for (ShaderPatchCompiler.Probe probe : program.probes()) { + boolean present = switch (probe.type()) { + case REQUIRE_HASH, FORBID_HASH -> index.availableHashes().contains(probe.value()); + case REQUIRE_IDENT, FORBID_IDENT -> hasIdentifier(index.source(), probe.value()); + case REQUIRE_FUNCTION, FORBID_FUNCTION -> findFunctionStart(index.source(), probe.value(), 1, false) >= 0; + case REQUIRE_CALL, FORBID_CALL -> hasCall(index.source(), probe.value()); + case REQUIRE_UNIFORM, FORBID_UNIFORM -> hasUniform(index.source(), probe.value()); + case REQUIRE_DEFINE, FORBID_DEFINE -> hasDefine(index.source(), probe.value()); + }; + if (probe.type().required() && !present) missing.add(probe.type().label() + " " + probe.value()); + else if (!probe.type().required() && present) forbidden.add(probe.type().label() + " " + probe.value()); + } + if (!missing.isEmpty()) throw new ShaderPatchCompiler.CompileException("Required structural probes are absent: " + String.join(", ", missing)); + if (!forbidden.isEmpty()) throw new ShaderPatchCompiler.CompileException("Forbidden structural probes are present: " + String.join(", ", forbidden)); + } + static int find(SourceIndex index, ShaderPatchCompiler.Selector selector) throws ShaderPatchCompiler.CompileException { + return switch (selector.kind()) { + case HASH -> findHash(index.hashes(), selector); + case IDENT -> findLine(index.source(), selector, line -> hasIdentifier(line, selector.value())); + case FUNCTION -> findFunctionStartChecked(index.source(), selector); + case FUNCTION_OPEN -> findFunctionOpenChecked(index.source(), selector); + case DEFINE -> findLine(index.source(), selector, line -> isDefine(line, selector.value())); + case DRAWBUFFERS -> findLine(index.source(), selector, line -> line.contains("DRAWBUFFERS")); + case STAGE_BEGIN -> findLine(index.source(), selector, line -> isStageBegin(line, selector.value())); + case ZONE -> findZone(index.source(), selector.value()); + }; + } + static int findHash(List hashes, ShaderPatchCompiler.Selector selector) throws ShaderPatchCompiler.CompileException { + int occurrence = 0; int result = -1; + for (int i = 0; i < hashes.size(); i++) { + if (!hashes.get(i).equals(selector.value())) continue; + occurrence++; + if (occurrence == selector.occurrence()) result = i; + } + if (result < 0) throw new ShaderPatchCompiler.CompileException("Anchor not found: hash " + selector.value() + "#" + selector.occurrence()); + if (selector.requireUnique() && occurrence != 1) throw new ShaderPatchCompiler.CompileException("Anchor is ambiguous; specify occurrence: hash " + selector.value()); + return result; + } + static int findLine(List source, ShaderPatchCompiler.Selector selector, LinePredicate predicate) throws ShaderPatchCompiler.CompileException { + int occurrence = 0; int result = -1; + for (int i = 0; i < source.size(); i++) { + if (!predicate.matches(source.get(i))) continue; + occurrence++; + if (occurrence == selector.occurrence()) result = i; + } + if (result < 0) throw new ShaderPatchCompiler.CompileException("Anchor not found: " + selector.kind().name().toLowerCase(Locale.ROOT) + " " + selector.value() + "#" + selector.occurrence()); + if (selector.requireUnique() && occurrence != 1) throw new ShaderPatchCompiler.CompileException("Anchor is ambiguous; specify occurrence: " + selector.kind().name().toLowerCase(Locale.ROOT) + " " + selector.value()); + return result; + } + static int findFunctionStartChecked(List source, ShaderPatchCompiler.Selector selector) throws ShaderPatchCompiler.CompileException { + int index = findFunctionStart(source, selector.value(), selector.occurrence(), selector.requireUnique()); + if (index < 0) throw new ShaderPatchCompiler.CompileException("Anchor not found: function " + selector.value() + "#" + selector.occurrence()); + return index; + } + static int findFunctionOpenChecked(List source, ShaderPatchCompiler.Selector selector) throws ShaderPatchCompiler.CompileException { + int start = findFunctionStartChecked(source, new ShaderPatchCompiler.Selector(ShaderPatchCompiler.SelectorKind.FUNCTION, selector.value(), selector.occurrence(), selector.requireUnique())); + for (int i = start; i < source.size(); i++) { + String line = stripLineComment(source.get(i)); + int brace = line.indexOf('{'); int semicolon = line.indexOf(';'); + if (brace >= 0 && (semicolon < 0 || brace < semicolon)) return i; + if (semicolon >= 0) break; + } + throw new ShaderPatchCompiler.CompileException("Function opening brace not found: " + selector.value() + "#" + selector.occurrence()); + } + static int findFunctionStart(List source, String functionName, int requestedOccurrence, boolean requireUnique) { + int occurrence = 0; int result = -1; + for (int i = 0; i < source.size(); i++) { + if (!looksLikeFunctionStart(source.get(i), functionName)) continue; + occurrence++; + if (occurrence == requestedOccurrence) result = i; + } + if (result < 0) return -1; + if (requireUnique && occurrence != 1) return -2; + return result; + } + static int findZone(List source, String zone) { + if (zone.equals("declarations")) { + for (int i = 0; i < source.size(); i++) { + String trimmed = source.get(i).trim(); + if (GLOBAL_DECLARATION.matcher(trimmed).matches()) return i; + } + } + for (int i = 0; i < source.size(); i++) if (looksLikeAnyFunctionStart(source.get(i))) return i; + return 0; + } + static boolean isStageBegin(String line, String stageMacro) { + String stripped = stripLineComment(line).trim(); + return stripped.equals("#ifdef " + stageMacro) || stripped.equals("#if defined " + stageMacro) || stripped.equals("#if defined(" + stageMacro + ")"); + } + static boolean looksLikeAnyFunctionStart(String line) { + String stripped = stripLineComment(line).trim(); + if (stripped.isEmpty() || stripped.startsWith("#")) return false; + return stripped.matches(".*\\b[A-Za-z_][A-Za-z0-9_]*\\s*\\(.*") && !stripped.endsWith(";"); + } + static boolean looksLikeFunctionStart(String line, String functionName) { + String stripped = stripLineComment(line).trim(); + if (stripped.isEmpty() || stripped.startsWith("#")) return false; + if (!hasIdentifier(stripped, functionName)) return false; + int nameIndex = stripped.indexOf(functionName); + int paren = stripped.indexOf('(', nameIndex + functionName.length()); + if (paren < 0) return false; + int semicolon = stripped.indexOf(';'); + return semicolon < 0 || semicolon > paren; + } + static boolean hasCall(List source, String name) { + String needle = name + "("; + for (String line : source) { String stripped = stripLineComment(line); if (stripped.contains(needle)) return true; } + return false; + } + static boolean hasUniform(List source, String name) { + for (String line : source) { String stripped = stripLineComment(line).trim(); if (stripped.startsWith("uniform ") && hasIdentifier(stripped, name)) return true; } + return false; + } + static boolean hasDefine(List source, String name) { + for (String line : source) if (isDefine(line, name)) return true; + return false; + } + static boolean isDefine(String line, String name) { + String stripped = stripLineComment(line).trim(); + if (!stripped.startsWith("#define")) return false; + String rest = stripped.substring("#define".length()).trim(); + if (rest.startsWith(name)) return rest.length() == name.length() || !isIdentifierPart(rest.charAt(name.length())); + return false; + } + static boolean hasIdentifier(List source, String identifier) { + for (String line : source) if (hasIdentifier(line, identifier)) return true; + return false; + } + static boolean hasIdentifier(String line, String identifier) { + String stripped = stripLineComment(line); + int from = 0; + while (from < stripped.length()) { + int index = stripped.indexOf(identifier, from); + if (index < 0) return false; + int before = index - 1; int after = index + identifier.length(); + boolean validBefore = before < 0 || !isIdentifierPart(stripped.charAt(before)); + boolean validAfter = after >= stripped.length() || !isIdentifierPart(stripped.charAt(after)); + if (validBefore && validAfter) return true; + from = index + identifier.length(); + } + return false; + } + static boolean isIdentifierPart(char character) { return Character.isLetterOrDigit(character) || character == '_'; } + static String stripLineComment(String line) { int comment = line.indexOf("//"); return comment >= 0 ? line.substring(0, comment) : line; } + interface LinePredicate { boolean matches(String line); } + record SourceIndex(ImmutableList source, List hashes, Set availableHashes) { + static SourceIndex create(ImmutableList source) { + ArrayList hashes = new ArrayList<>(source.size()); + Set available = new HashSet<>(source.size()); + for (String line : source) { String hash = ShaderPatchCompiler.signature(line); hashes.add(hash); available.add(hash); } + return new SourceIndex(source, List.copyOf(hashes), Set.copyOf(available)); + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/iris/patch/parse/PatchParseSupport.java b/mod/src/main/java/dev/loki/lovisual/render/iris/patch/parse/PatchParseSupport.java new file mode 100644 index 0000000..6c89f09 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/iris/patch/parse/PatchParseSupport.java @@ -0,0 +1,157 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.render.iris.patch; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.List; +import java.util.Locale; + +final class PatchParseSupport { + private PatchParseSupport() { + } + + static ShaderPatchCompiler.Operation readHashRangeOperation(String[] parts, BufferedReader reader, int lineNumber, String directive) throws IOException, ShaderPatchCompiler.CompileException { + if (parts.length < 3 || parts.length > 6) { + throw new ShaderPatchCompiler.CompileException("Invalid " + directive + " at line " + lineNumber); + } + ShaderPatchCompiler.Selector start = new ShaderPatchCompiler.Selector( + ShaderPatchCompiler.SelectorKind.HASH, + validateHash(parts[1], lineNumber), + readPositive(parts, 3, 1, lineNumber), + parts.length <= 3 + ); + ShaderPatchCompiler.Selector end = new ShaderPatchCompiler.Selector( + ShaderPatchCompiler.SelectorKind.HASH, + validateHash(parts[2], lineNumber), + readPositive(parts, 4, 1, lineNumber), + parts.length <= 4 + ); + boolean includeEnd = parts.length >= 6 && Boolean.parseBoolean(parts[5]); + return new ShaderPatchCompiler.Operation( + ShaderPatchCompiler.OperationType.valueOf(directive.toUpperCase(Locale.ROOT)), + start, + end, + includeEnd, + readPayload(reader, lineNumber) + ); + } + + static ShaderPatchCompiler.Operation readSingleSelectorOperation(String[] parts, BufferedReader reader, int lineNumber, String directive, ShaderPatchCompiler.SelectorKind kind) throws IOException, ShaderPatchCompiler.CompileException { + ShaderPatchCompiler.OperationType operationType = operationType(directive); + ShaderPatchCompiler.Selector selector = readSelector(parts, kind, lineNumber); + return new ShaderPatchCompiler.Operation( + operationType, + selector, + null, + false, + operationType == ShaderPatchCompiler.OperationType.REMOVE_LINE ? List.of() : readPayload(reader, lineNumber) + ); + } + + static ShaderPatchCompiler.OperationType operationType(String directive) throws ShaderPatchCompiler.CompileException { + if (directive.startsWith("insert_before")) return ShaderPatchCompiler.OperationType.INSERT_BEFORE; + if (directive.startsWith("insert_after")) return ShaderPatchCompiler.OperationType.INSERT_AFTER; + if (directive.startsWith("replace_line")) return ShaderPatchCompiler.OperationType.REPLACE_LINE; + if (directive.startsWith("remove_line")) return ShaderPatchCompiler.OperationType.REMOVE_LINE; + throw new ShaderPatchCompiler.CompileException("Unsupported operation directive @" + directive); + } + + static ShaderPatchCompiler.Selector readZoneSelector(String[] parts, int lineNumber) throws ShaderPatchCompiler.CompileException { + if (parts.length != 2) { + throw new ShaderPatchCompiler.CompileException("Invalid insert_zone directive at line " + lineNumber); + } + String zone = parts[1].toLowerCase(Locale.ROOT); + if (!zone.equals("declarations") && !zone.equals("functions")) { + throw new ShaderPatchCompiler.CompileException("Unsupported insert_zone " + parts[1] + " at line " + lineNumber); + } + return new ShaderPatchCompiler.Selector(ShaderPatchCompiler.SelectorKind.ZONE, zone, 1, false); + } + + static ShaderPatchCompiler.Selector readSelector(String[] parts, ShaderPatchCompiler.SelectorKind kind, int lineNumber) throws ShaderPatchCompiler.CompileException { + if (kind == ShaderPatchCompiler.SelectorKind.DRAWBUFFERS) { + if (parts.length < 1 || parts.length > 2) { + throw new ShaderPatchCompiler.CompileException("Invalid drawbuffers selector at line " + lineNumber); + } + return new ShaderPatchCompiler.Selector(ShaderPatchCompiler.SelectorKind.DRAWBUFFERS, "DRAWBUFFERS", readPositive(parts, 1, 1, lineNumber), parts.length == 1); + } + if (parts.length < 2 || parts.length > 3) { + throw new ShaderPatchCompiler.CompileException("Invalid selector at line " + lineNumber); + } + String value = kind == ShaderPatchCompiler.SelectorKind.HASH ? validateHash(parts[1], lineNumber) : validateName(parts[1], lineNumber); + return new ShaderPatchCompiler.Selector(kind, value, readPositive(parts, 2, 1, lineNumber), parts.length == 2); + } + + static ShaderPatchCompiler.Selector readHashSelector(String[] parts, int signatureIndex, int lineNumber) throws ShaderPatchCompiler.CompileException { + if (parts.length < signatureIndex + 1 || parts.length > signatureIndex + 2) { + throw new ShaderPatchCompiler.CompileException("Invalid hash anchor at line " + lineNumber); + } + return new ShaderPatchCompiler.Selector( + ShaderPatchCompiler.SelectorKind.HASH, + validateHash(parts[signatureIndex], lineNumber), + readPositive(parts, signatureIndex + 1, 1, lineNumber), + parts.length == signatureIndex + 1 + ); + } + + static String readHash(String[] parts, int index, int lineNumber) throws ShaderPatchCompiler.CompileException { + if (parts.length != index + 1) { + throw new ShaderPatchCompiler.CompileException("Invalid hash directive at line " + lineNumber); + } + return validateHash(parts[index], lineNumber); + } + + static String readName(String[] parts, int index, int lineNumber) throws ShaderPatchCompiler.CompileException { + if (parts.length != index + 1) { + throw new ShaderPatchCompiler.CompileException("Invalid identifier directive at line " + lineNumber); + } + return validateName(parts[index], lineNumber); + } + + static String validateHash(String hash, int lineNumber) throws ShaderPatchCompiler.CompileException { + String normalized = hash.toLowerCase(Locale.ROOT); + if (!normalized.matches("[0-9a-f]{64}")) { + throw new ShaderPatchCompiler.CompileException("Invalid structural hash at line " + lineNumber); + } + return normalized; + } + + static String validateName(String name, int lineNumber) throws ShaderPatchCompiler.CompileException { + if (!name.matches("[A-Za-z_][A-Za-z0-9_]*")) { + throw new ShaderPatchCompiler.CompileException("Invalid identifier '" + name + "' at line " + lineNumber); + } + return name; + } + + static int readPositive(String[] parts, int index, int fallback, int lineNumber) throws ShaderPatchCompiler.CompileException { + if (parts.length <= index) { + return fallback; + } + try { + int value = Integer.parseInt(parts[index]); + if (value < 1) { + throw new NumberFormatException(); + } + return value; + } catch (NumberFormatException e) { + throw new ShaderPatchCompiler.CompileException("Invalid occurrence at line " + lineNumber); + } + } + + static List readPayload(BufferedReader reader, int directiveLine) throws IOException, ShaderPatchCompiler.CompileException { + java.util.ArrayList payload = new java.util.ArrayList<>(); + String line; + while ((line = reader.readLine()) != null) { + if (line.trim().equals("@end")) { + return List.copyOf(payload); + } + payload.add(line); + } + throw new ShaderPatchCompiler.CompileException("Unterminated payload after line " + directiveLine); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/iris/patch/parse/PatchParser.java b/mod/src/main/java/dev/loki/lovisual/render/iris/patch/parse/PatchParser.java new file mode 100644 index 0000000..1fafa28 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/render/iris/patch/parse/PatchParser.java @@ -0,0 +1,110 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.render.iris.patch; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +final class PatchParser { + private PatchParser() { + } + + static ShaderPatchCompiler.PatchProgram parse(Reader source) throws IOException, ShaderPatchCompiler.CompileException { + BufferedReader reader = source instanceof BufferedReader buffered ? buffered : new BufferedReader(source); + ArrayList probes = new ArrayList<>(); + ArrayList operations = new ArrayList<>(); + int schemaVersion = 1; + String line; + int lineNumber = 0; + + while ((line = reader.readLine()) != null) { + lineNumber++; + String trimmed = line.trim(); + if (trimmed.isEmpty() || trimmed.startsWith(";")) { + continue; + } + if (!trimmed.startsWith("@")) { + throw new ShaderPatchCompiler.CompileException("Expected directive at line " + lineNumber); + } + + String[] parts = trimmed.substring(1).split("\\s+"); + String directive = parts[0]; + switch (directive) { + case "patch" -> { + if (parts.length != 2) { + throw new ShaderPatchCompiler.CompileException("Invalid patch schema directive at line " + lineNumber); + } + try { + schemaVersion = Integer.parseInt(parts[1]); + } catch (NumberFormatException e) { + throw new ShaderPatchCompiler.CompileException("Invalid patch schema at line " + lineNumber); + } + if (schemaVersion != 1 && schemaVersion != 2) { + throw new ShaderPatchCompiler.CompileException("Unsupported patch schema " + schemaVersion + " at line " + lineNumber); + } + } + case "require" -> probes.add(new ShaderPatchCompiler.Probe(ShaderPatchCompiler.ProbeType.REQUIRE_HASH, PatchParseSupport.readHash(parts, 1, lineNumber))); + case "forbid" -> probes.add(new ShaderPatchCompiler.Probe(ShaderPatchCompiler.ProbeType.FORBID_HASH, PatchParseSupport.readHash(parts, 1, lineNumber))); + case "require_hash" -> probes.add(new ShaderPatchCompiler.Probe(ShaderPatchCompiler.ProbeType.REQUIRE_HASH, PatchParseSupport.readHash(parts, 1, lineNumber))); + case "forbid_hash" -> probes.add(new ShaderPatchCompiler.Probe(ShaderPatchCompiler.ProbeType.FORBID_HASH, PatchParseSupport.readHash(parts, 1, lineNumber))); + case "require_ident" -> probes.add(new ShaderPatchCompiler.Probe(ShaderPatchCompiler.ProbeType.REQUIRE_IDENT, PatchParseSupport.readName(parts, 1, lineNumber))); + case "forbid_ident" -> probes.add(new ShaderPatchCompiler.Probe(ShaderPatchCompiler.ProbeType.FORBID_IDENT, PatchParseSupport.readName(parts, 1, lineNumber))); + case "require_function" -> probes.add(new ShaderPatchCompiler.Probe(ShaderPatchCompiler.ProbeType.REQUIRE_FUNCTION, PatchParseSupport.readName(parts, 1, lineNumber))); + case "forbid_function" -> probes.add(new ShaderPatchCompiler.Probe(ShaderPatchCompiler.ProbeType.FORBID_FUNCTION, PatchParseSupport.readName(parts, 1, lineNumber))); + case "require_call" -> probes.add(new ShaderPatchCompiler.Probe(ShaderPatchCompiler.ProbeType.REQUIRE_CALL, PatchParseSupport.readName(parts, 1, lineNumber))); + case "forbid_call" -> probes.add(new ShaderPatchCompiler.Probe(ShaderPatchCompiler.ProbeType.FORBID_CALL, PatchParseSupport.readName(parts, 1, lineNumber))); + case "require_uniform" -> probes.add(new ShaderPatchCompiler.Probe(ShaderPatchCompiler.ProbeType.REQUIRE_UNIFORM, PatchParseSupport.readName(parts, 1, lineNumber))); + case "forbid_uniform" -> probes.add(new ShaderPatchCompiler.Probe(ShaderPatchCompiler.ProbeType.FORBID_UNIFORM, PatchParseSupport.readName(parts, 1, lineNumber))); + case "require_define" -> probes.add(new ShaderPatchCompiler.Probe(ShaderPatchCompiler.ProbeType.REQUIRE_DEFINE, PatchParseSupport.readName(parts, 1, lineNumber))); + case "forbid_define" -> probes.add(new ShaderPatchCompiler.Probe(ShaderPatchCompiler.ProbeType.FORBID_DEFINE, PatchParseSupport.readName(parts, 1, lineNumber))); + case "insert_before", "insert_after", "replace_line", "remove_line" -> { + ShaderPatchCompiler.Selector selector = PatchParseSupport.readHashSelector(parts, 1, lineNumber); + operations.add(new ShaderPatchCompiler.Operation( + ShaderPatchCompiler.OperationType.valueOf(directive.toUpperCase(Locale.ROOT)), + selector, + null, + false, + directive.equals("remove_line") ? List.of() : PatchParseSupport.readPayload(reader, lineNumber) + )); + } + case "replace_range", "replace_between" -> operations.add(PatchParseSupport.readHashRangeOperation(parts, reader, lineNumber, directive)); + case "insert_zone" -> operations.add(new ShaderPatchCompiler.Operation( + ShaderPatchCompiler.OperationType.INSERT_BEFORE, + PatchParseSupport.readZoneSelector(parts, lineNumber), + null, + false, + PatchParseSupport.readPayload(reader, lineNumber) + )); + case "insert_before_hash", "insert_after_hash", "replace_line_hash", "remove_line_hash" -> + operations.add(PatchParseSupport.readSingleSelectorOperation(parts, reader, lineNumber, directive, ShaderPatchCompiler.SelectorKind.HASH)); + case "insert_before_ident", "insert_after_ident", "replace_line_ident", "remove_line_ident" -> + operations.add(PatchParseSupport.readSingleSelectorOperation(parts, reader, lineNumber, directive, ShaderPatchCompiler.SelectorKind.IDENT)); + case "insert_before_function", "replace_line_function", "remove_line_function" -> + operations.add(PatchParseSupport.readSingleSelectorOperation(parts, reader, lineNumber, directive, ShaderPatchCompiler.SelectorKind.FUNCTION)); + case "insert_after_function_open" -> + operations.add(PatchParseSupport.readSingleSelectorOperation(parts, reader, lineNumber, directive, ShaderPatchCompiler.SelectorKind.FUNCTION_OPEN)); + case "insert_before_define", "insert_after_define", "replace_line_define", "remove_line_define" -> + operations.add(PatchParseSupport.readSingleSelectorOperation(parts, reader, lineNumber, directive, ShaderPatchCompiler.SelectorKind.DEFINE)); + case "insert_before_drawbuffers", "insert_after_drawbuffers", "replace_line_drawbuffers", "remove_line_drawbuffers" -> + operations.add(PatchParseSupport.readSingleSelectorOperation(parts, reader, lineNumber, directive, ShaderPatchCompiler.SelectorKind.DRAWBUFFERS)); + case "insert_after_stage_begin" -> + operations.add(PatchParseSupport.readSingleSelectorOperation(parts, reader, lineNumber, directive, ShaderPatchCompiler.SelectorKind.STAGE_BEGIN)); + default -> throw new ShaderPatchCompiler.CompileException("Unknown directive @" + directive + " at line " + lineNumber); + } + } + + if (operations.isEmpty()) { + throw new ShaderPatchCompiler.CompileException("Patch contains no operations"); + } + return new ShaderPatchCompiler.PatchProgram(schemaVersion, List.copyOf(probes), List.copyOf(operations)); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/iris/runtime/IrisRuntime.java b/mod/src/main/java/dev/loki/lovisual/render/iris/runtime/IrisRuntime.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/iris/runtime/IrisRuntime.java rename to mod/src/main/java/dev/loki/lovisual/render/iris/runtime/IrisRuntime.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/iris/runtime/IrisRuntimeBridge.java b/mod/src/main/java/dev/loki/lovisual/render/iris/runtime/IrisRuntimeBridge.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/iris/runtime/IrisRuntimeBridge.java rename to mod/src/main/java/dev/loki/lovisual/render/iris/runtime/IrisRuntimeBridge.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/iris/runtime/IrisRuntimeSnapshot.java b/mod/src/main/java/dev/loki/lovisual/render/iris/runtime/IrisRuntimeSnapshot.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/iris/runtime/IrisRuntimeSnapshot.java rename to mod/src/main/java/dev/loki/lovisual/render/iris/runtime/IrisRuntimeSnapshot.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/iris/runtime/LoVisualIrisUniforms.java b/mod/src/main/java/dev/loki/lovisual/render/iris/runtime/LoVisualIrisUniforms.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/iris/runtime/LoVisualIrisUniforms.java rename to mod/src/main/java/dev/loki/lovisual/render/iris/runtime/LoVisualIrisUniforms.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/iris/scene/IrisFinalizedSceneRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/iris/scene/IrisFinalizedSceneRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/iris/scene/IrisFinalizedSceneRenderer.java rename to mod/src/main/java/dev/loki/lovisual/render/iris/scene/IrisFinalizedSceneRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/iris/scene/IrisSceneDepth.java b/mod/src/main/java/dev/loki/lovisual/render/iris/scene/IrisSceneDepth.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/iris/scene/IrisSceneDepth.java rename to mod/src/main/java/dev/loki/lovisual/render/iris/scene/IrisSceneDepth.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/iris/scene/IrisSecondHandScene.java b/mod/src/main/java/dev/loki/lovisual/render/iris/scene/IrisSecondHandScene.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/iris/scene/IrisSecondHandScene.java rename to mod/src/main/java/dev/loki/lovisual/render/iris/scene/IrisSecondHandScene.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/iris/state/IrisHandMaskState.java b/mod/src/main/java/dev/loki/lovisual/render/iris/state/IrisHandMaskState.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/iris/state/IrisHandMaskState.java rename to mod/src/main/java/dev/loki/lovisual/render/iris/state/IrisHandMaskState.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/iris/state/IrisLoVisualFrameHooks.java b/mod/src/main/java/dev/loki/lovisual/render/iris/state/IrisLoVisualFrameHooks.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/iris/state/IrisLoVisualFrameHooks.java rename to mod/src/main/java/dev/loki/lovisual/render/iris/state/IrisLoVisualFrameHooks.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/sky/CustomSkyboxRenderer.java b/mod/src/main/java/dev/loki/lovisual/render/sky/CustomSkyboxRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/sky/CustomSkyboxRenderer.java rename to mod/src/main/java/dev/loki/lovisual/render/sky/CustomSkyboxRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/sodium/bridge/SodiumFrameContext.java b/mod/src/main/java/dev/loki/lovisual/render/sodium/bridge/SodiumFrameContext.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/sodium/bridge/SodiumFrameContext.java rename to mod/src/main/java/dev/loki/lovisual/render/sodium/bridge/SodiumFrameContext.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/sodium/bridge/SodiumRenderBridge.java b/mod/src/main/java/dev/loki/lovisual/render/sodium/bridge/SodiumRenderBridge.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/sodium/bridge/SodiumRenderBridge.java rename to mod/src/main/java/dev/loki/lovisual/render/sodium/bridge/SodiumRenderBridge.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/sodium/bridge/SodiumShaderWorkarounds.java b/mod/src/main/java/dev/loki/lovisual/render/sodium/bridge/SodiumShaderWorkarounds.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/sodium/bridge/SodiumShaderWorkarounds.java rename to mod/src/main/java/dev/loki/lovisual/render/sodium/bridge/SodiumShaderWorkarounds.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/sodium/interop/SodiumTerrainInterop.java b/mod/src/main/java/dev/loki/lovisual/render/sodium/interop/SodiumTerrainInterop.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/sodium/interop/SodiumTerrainInterop.java rename to mod/src/main/java/dev/loki/lovisual/render/sodium/interop/SodiumTerrainInterop.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/sodium/interop/SodiumTerrainInteropStatsSnapshot.java b/mod/src/main/java/dev/loki/lovisual/render/sodium/interop/SodiumTerrainInteropStatsSnapshot.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/sodium/interop/SodiumTerrainInteropStatsSnapshot.java rename to mod/src/main/java/dev/loki/lovisual/render/sodium/interop/SodiumTerrainInteropStatsSnapshot.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/sodium/terrain/LoVisualChunkMeshAttributes.java b/mod/src/main/java/dev/loki/lovisual/render/sodium/terrain/LoVisualChunkMeshAttributes.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/sodium/terrain/LoVisualChunkMeshAttributes.java rename to mod/src/main/java/dev/loki/lovisual/render/sodium/terrain/LoVisualChunkMeshAttributes.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/sodium/terrain/LoVisualChunkMeshFormats.java b/mod/src/main/java/dev/loki/lovisual/render/sodium/terrain/LoVisualChunkMeshFormats.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/sodium/terrain/LoVisualChunkMeshFormats.java rename to mod/src/main/java/dev/loki/lovisual/render/sodium/terrain/LoVisualChunkMeshFormats.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/sodium/terrain/vertex/LoVisualChunkVertexExtension.java b/mod/src/main/java/dev/loki/lovisual/render/sodium/terrain/vertex/LoVisualChunkVertexExtension.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/sodium/terrain/vertex/LoVisualChunkVertexExtension.java rename to mod/src/main/java/dev/loki/lovisual/render/sodium/terrain/vertex/LoVisualChunkVertexExtension.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/sodium/terrain/vertex/LoVisualModelVertexType.java b/mod/src/main/java/dev/loki/lovisual/render/sodium/terrain/vertex/LoVisualModelVertexType.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/sodium/terrain/vertex/LoVisualModelVertexType.java rename to mod/src/main/java/dev/loki/lovisual/render/sodium/terrain/vertex/LoVisualModelVertexType.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/sodium/terrain/vertex/LoVisualTerrainVertex.java b/mod/src/main/java/dev/loki/lovisual/render/sodium/terrain/vertex/LoVisualTerrainVertex.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/sodium/terrain/vertex/LoVisualTerrainVertex.java rename to mod/src/main/java/dev/loki/lovisual/render/sodium/terrain/vertex/LoVisualTerrainVertex.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/sodium/visibility/SodiumSectionVisibilityProvider.java b/mod/src/main/java/dev/loki/lovisual/render/sodium/visibility/SodiumSectionVisibilityProvider.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/sodium/visibility/SodiumSectionVisibilityProvider.java rename to mod/src/main/java/dev/loki/lovisual/render/sodium/visibility/SodiumSectionVisibilityProvider.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/sodium/visibility/SodiumVisibilityStatsSnapshot.java b/mod/src/main/java/dev/loki/lovisual/render/sodium/visibility/SodiumVisibilityStatsSnapshot.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/sodium/visibility/SodiumVisibilityStatsSnapshot.java rename to mod/src/main/java/dev/loki/lovisual/render/sodium/visibility/SodiumVisibilityStatsSnapshot.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/FastFps.java b/mod/src/main/java/dev/loki/lovisual/util/FastFps.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/FastFps.java rename to mod/src/main/java/dev/loki/lovisual/util/FastFps.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/LegacyHudNotifier.java b/mod/src/main/java/dev/loki/lovisual/util/LegacyHudNotifier.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/LegacyHudNotifier.java rename to mod/src/main/java/dev/loki/lovisual/util/LegacyHudNotifier.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/NarratorBlocker.java b/mod/src/main/java/dev/loki/lovisual/util/NarratorBlocker.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/NarratorBlocker.java rename to mod/src/main/java/dev/loki/lovisual/util/NarratorBlocker.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/OmniItemUtils.java b/mod/src/main/java/dev/loki/lovisual/util/OmniItemUtils.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/OmniItemUtils.java rename to mod/src/main/java/dev/loki/lovisual/util/OmniItemUtils.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/AimPointUtil.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/AimPointUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/AimPointUtil.java rename to mod/src/main/java/dev/loki/lovisual/util/aiming/AimPointUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/RequestHandler.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/RequestHandler.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/RequestHandler.java rename to mod/src/main/java/dev/loki/lovisual/util/aiming/RequestHandler.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/RestrictedSingleUseAction.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/RestrictedSingleUseAction.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/RestrictedSingleUseAction.java rename to mod/src/main/java/dev/loki/lovisual/util/aiming/RestrictedSingleUseAction.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/data/Rotation.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/data/Rotation.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/data/Rotation.java rename to mod/src/main/java/dev/loki/lovisual/util/aiming/data/Rotation.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/data/RotationDelta.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/data/RotationDelta.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/data/RotationDelta.java rename to mod/src/main/java/dev/loki/lovisual/util/aiming/data/RotationDelta.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/data/RotationWithVector.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/data/RotationWithVector.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/data/RotationWithVector.java rename to mod/src/main/java/dev/loki/lovisual/util/aiming/data/RotationWithVector.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/features/MovementCorrection.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/MovementCorrection.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/features/MovementCorrection.java rename to mod/src/main/java/dev/loki/lovisual/util/aiming/features/MovementCorrection.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/features/processors/FailRotationProcessor.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/FailRotationProcessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/features/processors/FailRotationProcessor.java rename to mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/FailRotationProcessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/features/processors/RotationProcessor.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/RotationProcessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/features/processors/RotationProcessor.java rename to mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/RotationProcessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/AngleSmooth.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/AngleSmooth.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/AngleSmooth.java rename to mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/AngleSmooth.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/FactorAngleSmooth.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/FactorAngleSmooth.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/FactorAngleSmooth.java rename to mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/FactorAngleSmooth.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/NoneAngleSmooth.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/NoneAngleSmooth.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/NoneAngleSmooth.java rename to mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/NoneAngleSmooth.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/AccelerationAngleSmooth.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/AccelerationAngleSmooth.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/AccelerationAngleSmooth.java rename to mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/AccelerationAngleSmooth.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/InterpolationAngleSmooth.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/InterpolationAngleSmooth.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/InterpolationAngleSmooth.java rename to mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/InterpolationAngleSmooth.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/LinearAngleSmooth.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/LinearAngleSmooth.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/LinearAngleSmooth.java rename to mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/LinearAngleSmooth.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/curves/SigmoidAngleSmooth.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/curves/SigmoidAngleSmooth.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/curves/SigmoidAngleSmooth.java rename to mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/curves/SigmoidAngleSmooth.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/curves/SmartAngleSmooth.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/curves/SmartAngleSmooth.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/curves/SmartAngleSmooth.java rename to mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/curves/SmartAngleSmooth.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/curves/SpookyAngleSmooth.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/curves/SpookyAngleSmooth.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/curves/SpookyAngleSmooth.java rename to mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/anglesmooth/impl/curves/SpookyAngleSmooth.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/features/processors/impl/LazyAimProcessor.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/impl/LazyAimProcessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/features/processors/impl/LazyAimProcessor.java rename to mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/impl/LazyAimProcessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/features/processors/impl/MovingRotationProcessor.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/impl/MovingRotationProcessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/features/processors/impl/MovingRotationProcessor.java rename to mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/impl/MovingRotationProcessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/features/processors/impl/ShakeAimProcessor.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/impl/ShakeAimProcessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/features/processors/impl/ShakeAimProcessor.java rename to mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/impl/ShakeAimProcessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/features/processors/impl/SnapAssistAimProcessor.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/impl/SnapAssistAimProcessor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/features/processors/impl/SnapAssistAimProcessor.java rename to mod/src/main/java/dev/loki/lovisual/util/aiming/features/processors/impl/SnapAssistAimProcessor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/point/PointInsideBox.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/point/PointInsideBox.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/point/PointInsideBox.java rename to mod/src/main/java/dev/loki/lovisual/util/aiming/point/PointInsideBox.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/point/PointTracker.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/point/PointTracker.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/point/PointTracker.java rename to mod/src/main/java/dev/loki/lovisual/util/aiming/point/PointTracker.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/preference/LeastDifferencePreference.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/preference/LeastDifferencePreference.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/preference/LeastDifferencePreference.java rename to mod/src/main/java/dev/loki/lovisual/util/aiming/preference/LeastDifferencePreference.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/preference/RotationPreference.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/preference/RotationPreference.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/preference/RotationPreference.java rename to mod/src/main/java/dev/loki/lovisual/util/aiming/preference/RotationPreference.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/raytrace/RotationRaytrace.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/raytrace/RotationRaytrace.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/raytrace/RotationRaytrace.java rename to mod/src/main/java/dev/loki/lovisual/util/aiming/raytrace/RotationRaytrace.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/rotation/RotationManager.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/rotation/RotationManager.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/rotation/RotationManager.java rename to mod/src/main/java/dev/loki/lovisual/util/aiming/rotation/RotationManager.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/rotation/RotationTarget.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/rotation/RotationTarget.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/rotation/RotationTarget.java rename to mod/src/main/java/dev/loki/lovisual/util/aiming/rotation/RotationTarget.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/rotation/RotationUtil.java b/mod/src/main/java/dev/loki/lovisual/util/aiming/rotation/RotationUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/aiming/rotation/RotationUtil.java rename to mod/src/main/java/dev/loki/lovisual/util/aiming/rotation/RotationUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/EaseCurves.java b/mod/src/main/java/dev/loki/lovisual/util/animation/EaseCurves.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/EaseCurves.java rename to mod/src/main/java/dev/loki/lovisual/util/animation/EaseCurves.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/EquipProgressComposer.java b/mod/src/main/java/dev/loki/lovisual/util/animation/EquipProgressComposer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/EquipProgressComposer.java rename to mod/src/main/java/dev/loki/lovisual/util/animation/EquipProgressComposer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/SmoothValue.java b/mod/src/main/java/dev/loki/lovisual/util/animation/SmoothValue.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/SmoothValue.java rename to mod/src/main/java/dev/loki/lovisual/util/animation/SmoothValue.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/ThreeStageProgress.java b/mod/src/main/java/dev/loki/lovisual/util/animation/ThreeStageProgress.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/ThreeStageProgress.java rename to mod/src/main/java/dev/loki/lovisual/util/animation/ThreeStageProgress.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/BobMath.java b/mod/src/main/java/dev/loki/lovisual/util/animation/hand/BobMath.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/BobMath.java rename to mod/src/main/java/dev/loki/lovisual/util/animation/hand/BobMath.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/HandDynamics.java b/mod/src/main/java/dev/loki/lovisual/util/animation/hand/HandDynamics.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/HandDynamics.java rename to mod/src/main/java/dev/loki/lovisual/util/animation/hand/HandDynamics.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/HandPoseMath.java b/mod/src/main/java/dev/loki/lovisual/util/animation/hand/HandPoseMath.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/HandPoseMath.java rename to mod/src/main/java/dev/loki/lovisual/util/animation/hand/HandPoseMath.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/HeldAlongFist.java b/mod/src/main/java/dev/loki/lovisual/util/animation/hand/HeldAlongFist.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/HeldAlongFist.java rename to mod/src/main/java/dev/loki/lovisual/util/animation/hand/HeldAlongFist.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/state/HandAnimState.java b/mod/src/main/java/dev/loki/lovisual/util/animation/hand/state/HandAnimState.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/state/HandAnimState.java rename to mod/src/main/java/dev/loki/lovisual/util/animation/hand/state/HandAnimState.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/state/SwayState.java b/mod/src/main/java/dev/loki/lovisual/util/animation/hand/state/SwayState.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/state/SwayState.java rename to mod/src/main/java/dev/loki/lovisual/util/animation/hand/state/SwayState.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/block/bed/BedBlockUtil.java b/mod/src/main/java/dev/loki/lovisual/util/block/bed/BedBlockUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/block/bed/BedBlockUtil.java rename to mod/src/main/java/dev/loki/lovisual/util/block/bed/BedBlockUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/block/bed/BedwarsTeamColorUtil.java b/mod/src/main/java/dev/loki/lovisual/util/block/bed/BedwarsTeamColorUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/block/bed/BedwarsTeamColorUtil.java rename to mod/src/main/java/dev/loki/lovisual/util/block/bed/BedwarsTeamColorUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/block/bed/SelfBedMode.java b/mod/src/main/java/dev/loki/lovisual/util/block/bed/SelfBedMode.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/block/bed/SelfBedMode.java rename to mod/src/main/java/dev/loki/lovisual/util/block/bed/SelfBedMode.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/block/bed/SelfBedTracker.java b/mod/src/main/java/dev/loki/lovisual/util/block/bed/SelfBedTracker.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/block/bed/SelfBedTracker.java rename to mod/src/main/java/dev/loki/lovisual/util/block/bed/SelfBedTracker.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/block/render/BlockOutlineRenderer.java b/mod/src/main/java/dev/loki/lovisual/util/block/render/BlockOutlineRenderer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/block/render/BlockOutlineRenderer.java rename to mod/src/main/java/dev/loki/lovisual/util/block/render/BlockOutlineRenderer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/block/render/BlockOutlineShapeMode.java b/mod/src/main/java/dev/loki/lovisual/util/block/render/BlockOutlineShapeMode.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/block/render/BlockOutlineShapeMode.java rename to mod/src/main/java/dev/loki/lovisual/util/block/render/BlockOutlineShapeMode.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/block/render/RenderSectionBlockScanner.java b/mod/src/main/java/dev/loki/lovisual/util/block/render/RenderSectionBlockScanner.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/block/render/RenderSectionBlockScanner.java rename to mod/src/main/java/dev/loki/lovisual/util/block/render/RenderSectionBlockScanner.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/block/render/WorldSectionBlockScanner.java b/mod/src/main/java/dev/loki/lovisual/util/block/render/WorldSectionBlockScanner.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/block/render/WorldSectionBlockScanner.java rename to mod/src/main/java/dev/loki/lovisual/util/block/render/WorldSectionBlockScanner.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/block/scan/BlockDeobfuscationScanner.java b/mod/src/main/java/dev/loki/lovisual/util/block/scan/BlockDeobfuscationScanner.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/block/scan/BlockDeobfuscationScanner.java rename to mod/src/main/java/dev/loki/lovisual/util/block/scan/BlockDeobfuscationScanner.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/block/scan/BlockScanContext.java b/mod/src/main/java/dev/loki/lovisual/util/block/scan/BlockScanContext.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/block/scan/BlockScanContext.java rename to mod/src/main/java/dev/loki/lovisual/util/block/scan/BlockScanContext.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/block/scan/BlockScanMode.java b/mod/src/main/java/dev/loki/lovisual/util/block/scan/BlockScanMode.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/block/scan/BlockScanMode.java rename to mod/src/main/java/dev/loki/lovisual/util/block/scan/BlockScanMode.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/block/scan/BlockSearchUtil.java b/mod/src/main/java/dev/loki/lovisual/util/block/scan/BlockSearchUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/block/scan/BlockSearchUtil.java rename to mod/src/main/java/dev/loki/lovisual/util/block/scan/BlockSearchUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/click/AttackPressing.java b/mod/src/main/java/dev/loki/lovisual/util/click/AttackPressing.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/click/AttackPressing.java rename to mod/src/main/java/dev/loki/lovisual/util/click/AttackPressing.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/click/ClickHumanizer.java b/mod/src/main/java/dev/loki/lovisual/util/click/ClickHumanizer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/click/ClickHumanizer.java rename to mod/src/main/java/dev/loki/lovisual/util/click/ClickHumanizer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/click/ClickScheduler.java b/mod/src/main/java/dev/loki/lovisual/util/click/ClickScheduler.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/click/ClickScheduler.java rename to mod/src/main/java/dev/loki/lovisual/util/click/ClickScheduler.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/attack/AttackUtil.java b/mod/src/main/java/dev/loki/lovisual/util/combat/attack/AttackUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/attack/AttackUtil.java rename to mod/src/main/java/dev/loki/lovisual/util/combat/attack/AttackUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/attack/CombatStrikeController.java b/mod/src/main/java/dev/loki/lovisual/util/combat/attack/CombatStrikeController.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/attack/CombatStrikeController.java rename to mod/src/main/java/dev/loki/lovisual/util/combat/attack/CombatStrikeController.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/attack/ProtocolAttackExecutor.java b/mod/src/main/java/dev/loki/lovisual/util/combat/attack/ProtocolAttackExecutor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/attack/ProtocolAttackExecutor.java rename to mod/src/main/java/dev/loki/lovisual/util/combat/attack/ProtocolAttackExecutor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/hit/AttackHitPoint.java b/mod/src/main/java/dev/loki/lovisual/util/combat/hit/AttackHitPoint.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/hit/AttackHitPoint.java rename to mod/src/main/java/dev/loki/lovisual/util/combat/hit/AttackHitPoint.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/protocol/ClientProtocolVersion.java b/mod/src/main/java/dev/loki/lovisual/util/combat/protocol/ClientProtocolVersion.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/protocol/ClientProtocolVersion.java rename to mod/src/main/java/dev/loki/lovisual/util/combat/protocol/ClientProtocolVersion.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/protocol/ProtocolUtil.java b/mod/src/main/java/dev/loki/lovisual/util/combat/protocol/ProtocolUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/protocol/ProtocolUtil.java rename to mod/src/main/java/dev/loki/lovisual/util/combat/protocol/ProtocolUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/protocol/heuristics/CombatProtocolHeuristicSource.java b/mod/src/main/java/dev/loki/lovisual/util/combat/protocol/heuristics/CombatProtocolHeuristicSource.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/protocol/heuristics/CombatProtocolHeuristicSource.java rename to mod/src/main/java/dev/loki/lovisual/util/combat/protocol/heuristics/CombatProtocolHeuristicSource.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/protocol/heuristics/CombatProtocolHeuristics.java b/mod/src/main/java/dev/loki/lovisual/util/combat/protocol/heuristics/CombatProtocolHeuristics.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/protocol/heuristics/CombatProtocolHeuristics.java rename to mod/src/main/java/dev/loki/lovisual/util/combat/protocol/heuristics/CombatProtocolHeuristics.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/protocol/heuristics/CombatProtocolHeuristicsConfig.java b/mod/src/main/java/dev/loki/lovisual/util/combat/protocol/heuristics/CombatProtocolHeuristicsConfig.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/protocol/heuristics/CombatProtocolHeuristicsConfig.java rename to mod/src/main/java/dev/loki/lovisual/util/combat/protocol/heuristics/CombatProtocolHeuristicsConfig.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/protocol/vfp/VfpCompatibility.java b/mod/src/main/java/dev/loki/lovisual/util/combat/protocol/vfp/VfpCompatibility.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/protocol/vfp/VfpCompatibility.java rename to mod/src/main/java/dev/loki/lovisual/util/combat/protocol/vfp/VfpCompatibility.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/protocol/vfp/VfpCompatibility1_8.java b/mod/src/main/java/dev/loki/lovisual/util/combat/protocol/vfp/VfpCompatibility1_8.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/protocol/vfp/VfpCompatibility1_8.java rename to mod/src/main/java/dev/loki/lovisual/util/combat/protocol/vfp/VfpCompatibility1_8.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/rotation/CombatRotationModeUtil.java b/mod/src/main/java/dev/loki/lovisual/util/combat/rotation/CombatRotationModeUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/rotation/CombatRotationModeUtil.java rename to mod/src/main/java/dev/loki/lovisual/util/combat/rotation/CombatRotationModeUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/rotation/LethalStrikeCalculator.java b/mod/src/main/java/dev/loki/lovisual/util/combat/rotation/LethalStrikeCalculator.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/rotation/LethalStrikeCalculator.java rename to mod/src/main/java/dev/loki/lovisual/util/combat/rotation/LethalStrikeCalculator.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/rotation/RubberHandUseUtil.java b/mod/src/main/java/dev/loki/lovisual/util/combat/rotation/RubberHandUseUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/rotation/RubberHandUseUtil.java rename to mod/src/main/java/dev/loki/lovisual/util/combat/rotation/RubberHandUseUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/rotation/SprintController.java b/mod/src/main/java/dev/loki/lovisual/util/combat/rotation/SprintController.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/rotation/SprintController.java rename to mod/src/main/java/dev/loki/lovisual/util/combat/rotation/SprintController.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/tracking/AntiBotTracker.java b/mod/src/main/java/dev/loki/lovisual/util/combat/tracking/AntiBotTracker.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/tracking/AntiBotTracker.java rename to mod/src/main/java/dev/loki/lovisual/util/combat/tracking/AntiBotTracker.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/tracking/ReachMeter.java b/mod/src/main/java/dev/loki/lovisual/util/combat/tracking/ReachMeter.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/tracking/ReachMeter.java rename to mod/src/main/java/dev/loki/lovisual/util/combat/tracking/ReachMeter.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/entity/EagleUtil.java b/mod/src/main/java/dev/loki/lovisual/util/entity/EagleUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/entity/EagleUtil.java rename to mod/src/main/java/dev/loki/lovisual/util/entity/EagleUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/entity/FreecamEntity.java b/mod/src/main/java/dev/loki/lovisual/util/entity/FreecamEntity.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/entity/FreecamEntity.java rename to mod/src/main/java/dev/loki/lovisual/util/entity/FreecamEntity.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/entity/simulation/BoatSimulationCache.java b/mod/src/main/java/dev/loki/lovisual/util/entity/simulation/BoatSimulationCache.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/entity/simulation/BoatSimulationCache.java rename to mod/src/main/java/dev/loki/lovisual/util/entity/simulation/BoatSimulationCache.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/entity/simulation/MountedEntityPrediction.java b/mod/src/main/java/dev/loki/lovisual/util/entity/simulation/MountedEntityPrediction.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/entity/simulation/MountedEntityPrediction.java rename to mod/src/main/java/dev/loki/lovisual/util/entity/simulation/MountedEntityPrediction.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/entity/simulation/PositionExtrapolation.java b/mod/src/main/java/dev/loki/lovisual/util/entity/simulation/PositionExtrapolation.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/entity/simulation/PositionExtrapolation.java rename to mod/src/main/java/dev/loki/lovisual/util/entity/simulation/PositionExtrapolation.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/entity/simulation/PositionPredictor.java b/mod/src/main/java/dev/loki/lovisual/util/entity/simulation/PositionPredictor.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/entity/simulation/PositionPredictor.java rename to mod/src/main/java/dev/loki/lovisual/util/entity/simulation/PositionPredictor.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/input/KeyManager.java b/mod/src/main/java/dev/loki/lovisual/util/input/KeyManager.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/input/KeyManager.java rename to mod/src/main/java/dev/loki/lovisual/util/input/KeyManager.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/input/KeyUtil.java b/mod/src/main/java/dev/loki/lovisual/util/input/KeyUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/input/KeyUtil.java rename to mod/src/main/java/dev/loki/lovisual/util/input/KeyUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/item/enchant/EnchantMeta.java b/mod/src/main/java/dev/loki/lovisual/util/item/enchant/EnchantMeta.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/item/enchant/EnchantMeta.java rename to mod/src/main/java/dev/loki/lovisual/util/item/enchant/EnchantMeta.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/item/enchant/EnchantRegistry.java b/mod/src/main/java/dev/loki/lovisual/util/item/enchant/EnchantRegistry.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/item/enchant/EnchantRegistry.java rename to mod/src/main/java/dev/loki/lovisual/util/item/enchant/EnchantRegistry.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/item/enchant/EnchantUtil.java b/mod/src/main/java/dev/loki/lovisual/util/item/enchant/EnchantUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/item/enchant/EnchantUtil.java rename to mod/src/main/java/dev/loki/lovisual/util/item/enchant/EnchantUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/item/enchant/TopEnchantUtil.java b/mod/src/main/java/dev/loki/lovisual/util/item/enchant/TopEnchantUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/item/enchant/TopEnchantUtil.java rename to mod/src/main/java/dev/loki/lovisual/util/item/enchant/TopEnchantUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/item/food/FoodUtil.java b/mod/src/main/java/dev/loki/lovisual/util/item/food/FoodUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/item/food/FoodUtil.java rename to mod/src/main/java/dev/loki/lovisual/util/item/food/FoodUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/item/food/IllegalItemUtil.java b/mod/src/main/java/dev/loki/lovisual/util/item/food/IllegalItemUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/item/food/IllegalItemUtil.java rename to mod/src/main/java/dev/loki/lovisual/util/item/food/IllegalItemUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/item/rarity/ItemRarityColorProvider.java b/mod/src/main/java/dev/loki/lovisual/util/item/rarity/ItemRarityColorProvider.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/item/rarity/ItemRarityColorProvider.java rename to mod/src/main/java/dev/loki/lovisual/util/item/rarity/ItemRarityColorProvider.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/item/rarity/RarityColorConfig.java b/mod/src/main/java/dev/loki/lovisual/util/item/rarity/RarityColorConfig.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/item/rarity/RarityColorConfig.java rename to mod/src/main/java/dev/loki/lovisual/util/item/rarity/RarityColorConfig.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/item/rarity/RarityColorUtil.java b/mod/src/main/java/dev/loki/lovisual/util/item/rarity/RarityColorUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/item/rarity/RarityColorUtil.java rename to mod/src/main/java/dev/loki/lovisual/util/item/rarity/RarityColorUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/logging/DebugLog.java b/mod/src/main/java/dev/loki/lovisual/util/logging/DebugLog.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/logging/DebugLog.java rename to mod/src/main/java/dev/loki/lovisual/util/logging/DebugLog.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/logging/DebugMode.java b/mod/src/main/java/dev/loki/lovisual/util/logging/DebugMode.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/logging/DebugMode.java rename to mod/src/main/java/dev/loki/lovisual/util/logging/DebugMode.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/logging/ServerDumpUtil.java b/mod/src/main/java/dev/loki/lovisual/util/logging/ServerDumpUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/logging/ServerDumpUtil.java rename to mod/src/main/java/dev/loki/lovisual/util/logging/ServerDumpUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/map/MapPlayersCache.java b/mod/src/main/java/dev/loki/lovisual/util/map/MapPlayersCache.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/map/MapPlayersCache.java rename to mod/src/main/java/dev/loki/lovisual/util/map/MapPlayersCache.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/mctiers/MCTiersCache.java b/mod/src/main/java/dev/loki/lovisual/util/mctiers/MCTiersCache.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/mctiers/MCTiersCache.java rename to mod/src/main/java/dev/loki/lovisual/util/mctiers/MCTiersCache.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/mctiers/MCTiersTier.java b/mod/src/main/java/dev/loki/lovisual/util/mctiers/MCTiersTier.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/mctiers/MCTiersTier.java rename to mod/src/main/java/dev/loki/lovisual/util/mctiers/MCTiersTier.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/media/impl/DummyMediaPlayerInfo.java b/mod/src/main/java/dev/loki/lovisual/util/media/impl/DummyMediaPlayerInfo.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/media/impl/DummyMediaPlayerInfo.java rename to mod/src/main/java/dev/loki/lovisual/util/media/impl/DummyMediaPlayerInfo.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/media/impl/linux/LinuxMediaPlayerInfo.java b/mod/src/main/java/dev/loki/lovisual/util/media/impl/linux/LinuxMediaPlayerInfo.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/media/impl/linux/LinuxMediaPlayerInfo.java rename to mod/src/main/java/dev/loki/lovisual/util/media/impl/linux/LinuxMediaPlayerInfo.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/media/impl/linux/LinuxMediaSession.java b/mod/src/main/java/dev/loki/lovisual/util/media/impl/linux/LinuxMediaSession.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/media/impl/linux/LinuxMediaSession.java rename to mod/src/main/java/dev/loki/lovisual/util/media/impl/linux/LinuxMediaSession.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/media/impl/linux/dbus/Player.java b/mod/src/main/java/dev/loki/lovisual/util/media/impl/linux/dbus/Player.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/media/impl/linux/dbus/Player.java rename to mod/src/main/java/dev/loki/lovisual/util/media/impl/linux/dbus/Player.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/media/impl/win/WindowsMediaPlayerInfo.java b/mod/src/main/java/dev/loki/lovisual/util/media/impl/win/WindowsMediaPlayerInfo.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/media/impl/win/WindowsMediaPlayerInfo.java rename to mod/src/main/java/dev/loki/lovisual/util/media/impl/win/WindowsMediaPlayerInfo.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/media/impl/win/WindowsMediaSession.java b/mod/src/main/java/dev/loki/lovisual/util/media/impl/win/WindowsMediaSession.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/media/impl/win/WindowsMediaSession.java rename to mod/src/main/java/dev/loki/lovisual/util/media/impl/win/WindowsMediaSession.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/media/info/MediaInfo.java b/mod/src/main/java/dev/loki/lovisual/util/media/info/MediaInfo.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/media/info/MediaInfo.java rename to mod/src/main/java/dev/loki/lovisual/util/media/info/MediaInfo.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/media/info/MediaPlayerInfo.java b/mod/src/main/java/dev/loki/lovisual/util/media/info/MediaPlayerInfo.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/media/info/MediaPlayerInfo.java rename to mod/src/main/java/dev/loki/lovisual/util/media/info/MediaPlayerInfo.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/media/info/RepeatMode.java b/mod/src/main/java/dev/loki/lovisual/util/media/info/RepeatMode.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/media/info/RepeatMode.java rename to mod/src/main/java/dev/loki/lovisual/util/media/info/RepeatMode.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/media/session/IMediaSession.java b/mod/src/main/java/dev/loki/lovisual/util/media/session/IMediaSession.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/media/session/IMediaSession.java rename to mod/src/main/java/dev/loki/lovisual/util/media/session/IMediaSession.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/media/session/MediaSessionService.java b/mod/src/main/java/dev/loki/lovisual/util/media/session/MediaSessionService.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/media/session/MediaSessionService.java rename to mod/src/main/java/dev/loki/lovisual/util/media/session/MediaSessionService.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/network/control/BacktrackController.java b/mod/src/main/java/dev/loki/lovisual/util/network/control/BacktrackController.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/network/control/BacktrackController.java rename to mod/src/main/java/dev/loki/lovisual/util/network/control/BacktrackController.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/network/control/BlinkManager.java b/mod/src/main/java/dev/loki/lovisual/util/network/control/BlinkManager.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/network/control/BlinkManager.java rename to mod/src/main/java/dev/loki/lovisual/util/network/control/BlinkManager.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/network/control/FakeLagController.java b/mod/src/main/java/dev/loki/lovisual/util/network/control/FakeLagController.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/network/control/FakeLagController.java rename to mod/src/main/java/dev/loki/lovisual/util/network/control/FakeLagController.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/network/packet/PacketSnapshot.java b/mod/src/main/java/dev/loki/lovisual/util/network/packet/PacketSnapshot.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/network/packet/PacketSnapshot.java rename to mod/src/main/java/dev/loki/lovisual/util/network/packet/PacketSnapshot.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/network/packet/TransferOrigin.java b/mod/src/main/java/dev/loki/lovisual/util/network/packet/TransferOrigin.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/network/packet/TransferOrigin.java rename to mod/src/main/java/dev/loki/lovisual/util/network/packet/TransferOrigin.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/physics/TrinketPhysics.java b/mod/src/main/java/dev/loki/lovisual/util/physics/TrinketPhysics.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/physics/TrinketPhysics.java rename to mod/src/main/java/dev/loki/lovisual/util/physics/TrinketPhysics.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/effect/heuristics/StatusEffectHeuristics.java b/mod/src/main/java/dev/loki/lovisual/util/player/effect/heuristics/StatusEffectHeuristics.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/effect/heuristics/StatusEffectHeuristics.java rename to mod/src/main/java/dev/loki/lovisual/util/player/effect/heuristics/StatusEffectHeuristics.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/effect/heuristics/StatusEffectInference.java b/mod/src/main/java/dev/loki/lovisual/util/player/effect/heuristics/StatusEffectInference.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/effect/heuristics/StatusEffectInference.java rename to mod/src/main/java/dev/loki/lovisual/util/player/effect/heuristics/StatusEffectInference.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/effect/tracker/StatusEffectAccess.java b/mod/src/main/java/dev/loki/lovisual/util/player/effect/tracker/StatusEffectAccess.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/effect/tracker/StatusEffectAccess.java rename to mod/src/main/java/dev/loki/lovisual/util/player/effect/tracker/StatusEffectAccess.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/effect/tracker/StatusEffectTracker.java b/mod/src/main/java/dev/loki/lovisual/util/player/effect/tracker/StatusEffectTracker.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/effect/tracker/StatusEffectTracker.java rename to mod/src/main/java/dev/loki/lovisual/util/player/effect/tracker/StatusEffectTracker.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/effect/tracker/StatusEffectView.java b/mod/src/main/java/dev/loki/lovisual/util/player/effect/tracker/StatusEffectView.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/effect/tracker/StatusEffectView.java rename to mod/src/main/java/dev/loki/lovisual/util/player/effect/tracker/StatusEffectView.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/health/PlayerHealthResolver.java b/mod/src/main/java/dev/loki/lovisual/util/player/health/PlayerHealthResolver.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/health/PlayerHealthResolver.java rename to mod/src/main/java/dev/loki/lovisual/util/player/health/PlayerHealthResolver.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/health/PlayerSkinResolver.java b/mod/src/main/java/dev/loki/lovisual/util/player/health/PlayerSkinResolver.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/health/PlayerSkinResolver.java rename to mod/src/main/java/dev/loki/lovisual/util/player/health/PlayerSkinResolver.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/interaction/InteractionUtil.java b/mod/src/main/java/dev/loki/lovisual/util/player/interaction/InteractionUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/interaction/InteractionUtil.java rename to mod/src/main/java/dev/loki/lovisual/util/player/interaction/InteractionUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/interaction/NetworkStatsUtil.java b/mod/src/main/java/dev/loki/lovisual/util/player/interaction/NetworkStatsUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/interaction/NetworkStatsUtil.java rename to mod/src/main/java/dev/loki/lovisual/util/player/interaction/NetworkStatsUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/interaction/ResetAttackCooldown.java b/mod/src/main/java/dev/loki/lovisual/util/player/interaction/ResetAttackCooldown.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/interaction/ResetAttackCooldown.java rename to mod/src/main/java/dev/loki/lovisual/util/player/interaction/ResetAttackCooldown.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/interaction/ServerFlyUtils.java b/mod/src/main/java/dev/loki/lovisual/util/player/interaction/ServerFlyUtils.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/interaction/ServerFlyUtils.java rename to mod/src/main/java/dev/loki/lovisual/util/player/interaction/ServerFlyUtils.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/inventory/firework/FireworkUseController.java b/mod/src/main/java/dev/loki/lovisual/util/player/inventory/firework/FireworkUseController.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/inventory/firework/FireworkUseController.java rename to mod/src/main/java/dev/loki/lovisual/util/player/inventory/firework/FireworkUseController.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/movement/ElytraRecastUtil.java b/mod/src/main/java/dev/loki/lovisual/util/player/movement/ElytraRecastUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/movement/ElytraRecastUtil.java rename to mod/src/main/java/dev/loki/lovisual/util/player/movement/ElytraRecastUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/movement/EnvironmentMovementController.java b/mod/src/main/java/dev/loki/lovisual/util/player/movement/EnvironmentMovementController.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/movement/EnvironmentMovementController.java rename to mod/src/main/java/dev/loki/lovisual/util/player/movement/EnvironmentMovementController.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/movement/MovementUtil.java b/mod/src/main/java/dev/loki/lovisual/util/player/movement/MovementUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/movement/MovementUtil.java rename to mod/src/main/java/dev/loki/lovisual/util/player/movement/MovementUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/movement/PlayerSpeedUtil.java b/mod/src/main/java/dev/loki/lovisual/util/player/movement/PlayerSpeedUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/movement/PlayerSpeedUtil.java rename to mod/src/main/java/dev/loki/lovisual/util/player/movement/PlayerSpeedUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/simulation/LocalPlayerSimulation.java b/mod/src/main/java/dev/loki/lovisual/util/player/simulation/LocalPlayerSimulation.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/simulation/LocalPlayerSimulation.java rename to mod/src/main/java/dev/loki/lovisual/util/player/simulation/LocalPlayerSimulation.java diff --git a/mod/src/main/java/dev/loki/lovisual/util/player/simulation/PlayerMovementSimulation.java b/mod/src/main/java/dev/loki/lovisual/util/player/simulation/PlayerMovementSimulation.java new file mode 100644 index 0000000..cd65ddb --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/util/player/simulation/PlayerMovementSimulation.java @@ -0,0 +1,157 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.util.player.simulation; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.effect.MobEffects; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import dev.loki.lovisual.util.player.effect.tracker.StatusEffectAccess; +import dev.loki.lovisual.util.player.simulation.remote.RemotePlayerObservation; + +public abstract class PlayerMovementSimulation { + static final double STEP_HEIGHT = 0.5; + + final Player player; + final SimulatedInput input; + final float yaw; + final float pitch; + final boolean sprinting; + Vec3 pos; + Vec3 velocity; + AABB boundingBox; + boolean onGround; + boolean jumping; + boolean gliding; + double fallDistance; + boolean horizontalCollision; + boolean clipLedged; + boolean touchingWater; + boolean submergedInWater; + boolean swimming; + double waterHeight; + double lavaHeight; + int jumpingCooldown; + + protected PlayerMovementSimulation(Player player, SimulatedInput input) { + this.player = player; + this.pos = player.position(); + this.velocity = this.pos.subtract(player.xo, player.yo, player.zo); + this.boundingBox = player.getBoundingBox(); + this.yaw = player.getYRot(); + this.pitch = player.getXRot(); + this.sprinting = player.isSprinting(); + this.onGround = player.onGround(); + this.fallDistance = player.fallDistance; + this.jumping = !player.onGround(); + this.gliding = player.isFallFlying(); + this.input = input; + } + + void tick() { + clipLedged = false; + input.update(); + + FluidStateHelper.updateWaterState(this); + FluidStateHelper.updateSubmergedInWaterState(this); + FluidStateHelper.updateSwimming(this); + SneakAndClimbHelper.applyBubbleColumnEffects(this); + + if (jumpingCooldown > 0) { + jumpingCooldown--; + } + + jumping = input.jump; + + double vx = Math.abs(velocity.x) < 0.003 ? 0.0 : velocity.x; + double vy = Math.abs(velocity.y) < 0.003 ? 0.0 : velocity.y; + double vz = Math.abs(velocity.z) < 0.003 ? 0.0 : velocity.z; + velocity = new Vec3(vx, vy, vz); + + if (jumping && FluidPhysicsHelper.shouldSwimInFluids(this)) { + double fluidHeight = FluidPhysicsHelper.isInLava(this) ? lavaHeight : waterHeight; + boolean inWater = touchingWater && fluidHeight > 0.0; + double swimHeight = FluidPhysicsHelper.getSwimHeight(this); + + if (!inWater || onGround && !(fluidHeight > swimHeight)) { + if (!FluidPhysicsHelper.isInLava(this) || onGround && !(fluidHeight > swimHeight)) { + if ((onGround || inWater && fluidHeight <= swimHeight) && jumpingCooldown == 0) { + FluidPhysicsHelper.jump(this); + jumpingCooldown = 10; + } + } else { + FluidPhysicsHelper.swimUpward(this); + } + } else { + FluidPhysicsHelper.swimUpward(this); + } + } else { + jumpingCooldown = 0; + } + + if (StatusEffectAccess.has(player, MobEffects.SLOW_FALLING) || StatusEffectAccess.has(player, MobEffects.LEVITATION)) { + onGround = false; + } + + TravelHelper.travel(this, new Vec3(input.movementSideways * 0.98f, 0.0, input.movementForward * 0.98f)); + } + + protected boolean shouldUseRemoteFlightPhysics() { + return false; + } + + protected boolean shouldHoldVerticalMotion() { + return false; + } + + protected RemotePlayerObservation.MotionKind motionKind() { + return RemotePlayerObservation.MotionKind.NORMAL; + } + + BlockPos getVelocityAffectingPos() { + return BlockPos.containing(pos.x, boundingBox.minY - 0.5000001, pos.z); + } + + protected void setSimulatedVelocity(Vec3 velocity) { + this.velocity = velocity; + } + + protected void setSimulatedFallDistance(double fallDistance) { + this.fallDistance = fallDistance; + } + + protected void setSimulatedOnGround(boolean onGround) { + this.onGround = onGround; + } + + protected void setSimulatedGliding(boolean gliding) { + this.gliding = gliding; + } + + Vec3 position() { + return pos; + } + + double simulatedFallDistance() { + return fallDistance; + } + + Vec3 simulatedVelocity() { + return velocity; + } + + boolean simulatedOnGround() { + return onGround; + } + + boolean simulatedClipLedged() { + return clipLedged; + } + +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/simulation/PlayerSimulationCache.java b/mod/src/main/java/dev/loki/lovisual/util/player/simulation/PlayerSimulationCache.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/simulation/PlayerSimulationCache.java rename to mod/src/main/java/dev/loki/lovisual/util/player/simulation/PlayerSimulationCache.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/simulation/SimulatedInput.java b/mod/src/main/java/dev/loki/lovisual/util/player/simulation/SimulatedInput.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/simulation/SimulatedInput.java rename to mod/src/main/java/dev/loki/lovisual/util/player/simulation/SimulatedInput.java diff --git a/mod/src/main/java/dev/loki/lovisual/util/player/simulation/physics/FluidPhysicsHelper.java b/mod/src/main/java/dev/loki/lovisual/util/player/simulation/physics/FluidPhysicsHelper.java new file mode 100644 index 0000000..525ea0a --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/util/player/simulation/physics/FluidPhysicsHelper.java @@ -0,0 +1,151 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.util.player.simulation; + +import net.minecraft.core.BlockPos; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.Vec3; + +final class FluidPhysicsHelper { + + private FluidPhysicsHelper() { + } + + static void jump(PlayerMovementSimulation sim) { + double jumpVelocity = sim.player.getAttributeValue(Attributes.JUMP_STRENGTH) * getJumpVelocityMultiplier(sim) + + sim.player.getJumpBoostPower(); + if (jumpVelocity <= 1.0E-5) { + return; + } + + sim.velocity = new Vec3(sim.velocity.x, Math.max(jumpVelocity, sim.velocity.y), sim.velocity.z); + + if (sim.sprinting) { + float yawRadians = sim.yaw * Mth.DEG_TO_RAD; + sim.velocity = sim.velocity.add(-Mth.sin(yawRadians) * 0.2f, 0.0, Mth.cos(yawRadians) * 0.2f); + } + } + + static double getJumpVelocityMultiplier(PlayerMovementSimulation sim) { + BlockPos feetPos = BlockPos.containing(sim.pos); + float feet = sim.player.level().getBlockState(feetPos).getBlock().getJumpFactor(); + float floor = sim.player.level().getBlockState(sim.getVelocityAffectingPos()).getBlock().getJumpFactor(); + return feet == 1.0f ? floor : feet; + } + + static void travelInFluid(PlayerMovementSimulation sim, Vec3 movementInput) { + boolean falling = sim.velocity.y <= 0.0; + double y = sim.pos.y; + double gravity = TravelHelper.getEffectiveGravity(sim); + + if (sim.touchingWater) { + travelInWater(sim, movementInput, gravity, falling, y); + } else { + travelInLava(sim, movementInput, gravity, falling, y); + } + } + + static void travelInWater(PlayerMovementSimulation sim, Vec3 movementInput, double gravity, boolean falling, double y) { + float movementMultiplier = sim.sprinting ? 0.9f : 0.8f; + float acceleration = 0.02f; + float waterEfficiency = (float) sim.player.getAttributeValue(Attributes.WATER_MOVEMENT_EFFICIENCY); + if (!sim.onGround) { + waterEfficiency *= 0.5f; + } + + if (waterEfficiency > 0.0f) { + movementMultiplier += (0.54600006f - movementMultiplier) * waterEfficiency; + acceleration += (sim.player.getSpeed() - acceleration) * waterEfficiency; + } + + if (dev.loki.lovisual.util.player.effect.tracker.StatusEffectAccess.has(sim.player, net.minecraft.world.effect.MobEffects.DOLPHINS_GRACE)) { + movementMultiplier = 0.96f; + } + + TravelHelper.updateVelocity(sim, acceleration, movementInput); + SneakAndClimbHelper.move(sim, sim.velocity); + + Vec3 movedVelocity = sim.velocity; + if (sim.horizontalCollision && SneakAndClimbHelper.isClimbing(sim)) { + movedVelocity = new Vec3(movedVelocity.x, 0.2, movedVelocity.z); + } + + movedVelocity = movedVelocity.multiply(movementMultiplier, 0.8f, movementMultiplier); + sim.velocity = applyFluidMovingSpeed(sim, gravity, falling, movedVelocity); + resetVerticalVelocityInFluid(sim, y); + } + + static void travelInLava(PlayerMovementSimulation sim, Vec3 movementInput, double gravity, boolean falling, double y) { + TravelHelper.updateVelocity(sim, 0.02f, movementInput); + SneakAndClimbHelper.move(sim, sim.velocity); + if (sim.lavaHeight <= getSwimHeight(sim)) { + sim.velocity = sim.velocity.multiply(0.5, 0.8f, 0.5); + sim.velocity = applyFluidMovingSpeed(sim, gravity, falling, sim.velocity); + } else { + sim.velocity = sim.velocity.scale(0.5); + } + + if (gravity != 0.0) { + sim.velocity = sim.velocity.add(0.0, -gravity / 4.0, 0.0); + } + + resetVerticalVelocityInFluid(sim, y); + } + + static Vec3 applyFluidMovingSpeed(PlayerMovementSimulation sim, double gravity, boolean falling, Vec3 motion) { + if (gravity != 0.0 && !sim.sprinting) { + double y = falling && Math.abs(motion.y - 0.005) >= 0.003 && Math.abs(motion.y - gravity / 16.0) < 0.003 + ? -0.003 + : motion.y - gravity / 16.0; + return new Vec3(motion.x, y, motion.z); + } + + return motion; + } + + static void resetVerticalVelocityInFluid(PlayerMovementSimulation sim, double y) { + if (sim.horizontalCollision && doesNotCollide(sim, sim.velocity.x, sim.velocity.y + 0.6f - sim.pos.y + y, sim.velocity.z)) { + sim.velocity = new Vec3(sim.velocity.x, 0.3f, sim.velocity.z); + } + } + + static boolean doesNotCollide(PlayerMovementSimulation sim, double offsetX, double offsetY, double offsetZ) { + net.minecraft.world.phys.AABB moved = sim.boundingBox.move(offsetX, offsetY, offsetZ); + return sim.player.level().noCollision(sim.player, moved) && !sim.player.level().containsAnyLiquid(moved); + } + + static boolean shouldSwimInFluids(PlayerMovementSimulation sim) { + return !sim.player.getAbilities().flying; + } + + static boolean isInLava(PlayerMovementSimulation sim) { + return sim.lavaHeight > 0.0; + } + + static double getSwimHeight(PlayerMovementSimulation sim) { + return sim.player.getEyeHeight(sim.player.getPose()) < 0.4f ? 0.0 : 0.4; + } + + static void swimUpward(PlayerMovementSimulation sim) { + sim.velocity = sim.velocity.add(0.0, 0.04f, 0.0); + } + + static Vec3 applyWebSpeed(PlayerMovementSimulation sim, Vec3 motion) { + BlockState state = sim.player.level().getBlockState(BlockPos.containing(sim.pos)); + if (!state.is(Blocks.COBWEB)) { + return motion; + } + if (dev.loki.lovisual.util.player.effect.tracker.StatusEffectAccess.has(sim.player, net.minecraft.world.effect.MobEffects.WEAVING)) { + return motion.multiply(0.5, 0.25, 0.5); + } + return motion.multiply(0.25, 0.05, 0.25); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/util/player/simulation/physics/TravelHelper.java b/mod/src/main/java/dev/loki/lovisual/util/player/simulation/physics/TravelHelper.java new file mode 100644 index 0000000..810c41a --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/util/player/simulation/physics/TravelHelper.java @@ -0,0 +1,182 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.util.player.simulation; + +import net.minecraft.core.BlockPos; +import net.minecraft.util.Mth; +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.effect.MobEffects; +import net.minecraft.world.phys.Vec3; +import dev.loki.lovisual.mixins.accessors.entity.EntityInvoker; +import dev.loki.lovisual.util.player.effect.tracker.StatusEffectAccess; + +final class TravelHelper { + + private TravelHelper() { + } + + static void travel(PlayerMovementSimulation sim, Vec3 movementInput) { + if (sim.swimming && !sim.player.isPassenger()) { + double lookY = getRotationVector(sim).y; + double climb = lookY < -0.2 ? 0.085 : 0.06; + BlockPos fluidPos = BlockPos.containing(sim.pos.x, sim.pos.y + 0.9, sim.pos.z); + if (lookY <= 0.0 || sim.jumping || !sim.player.level().getFluidState(fluidPos).isEmpty()) { + sim.velocity = sim.velocity.add(0.0, (lookY - sim.velocity.y) * climb, 0.0); + } + } + + if ((sim.touchingWater || FluidPhysicsHelper.isInLava(sim)) && FluidPhysicsHelper.shouldSwimInFluids(sim)) { + FluidPhysicsHelper.travelInFluid(sim, movementInput); + return; + } + + if (sim.shouldUseRemoteFlightPhysics()) { + travelRemoteFlight(sim, movementInput); + return; + } + + if (sim.gliding) { + travelGliding(sim, movementInput); + return; + } + + travelMidAir(sim, movementInput); + } + + static void travelRemoteFlight(PlayerMovementSimulation sim, Vec3 movementInput) { + updateVelocity(sim, 0.045f, movementInput); + + if (sim.shouldHoldVerticalMotion() && Math.abs(sim.velocity.y) < 0.12) { + sim.velocity = new Vec3(sim.velocity.x, 0.0, sim.velocity.z); + } + + SneakAndClimbHelper.move(sim, sim.velocity); + + double yDrag = sim.shouldHoldVerticalMotion() ? 0.35 : 0.91; + sim.velocity = sim.velocity.multiply(0.91, yDrag, 0.91); + + if (sim.shouldHoldVerticalMotion() && Math.abs(sim.velocity.y) < 0.015) { + sim.velocity = new Vec3(sim.velocity.x, 0.0, sim.velocity.z); + } + + sim.fallDistance = 0.0; + sim.onGround = false; + } + + static void travelMidAir(PlayerMovementSimulation sim, Vec3 movementInput) { + BlockPos blockPos = sim.getVelocityAffectingPos(); + float slipperiness = sim.player.level().getBlockState(blockPos).getBlock().getFriction(); + float friction = sim.onGround ? slipperiness * 0.91f : 0.91f; + + Vec3 movedVelocity = applyMovementInput(sim, movementInput, slipperiness); + double y = movedVelocity.y; + MobEffectInstance levitation = StatusEffectAccess.get(sim.player, MobEffects.LEVITATION); + if (levitation != null) { + y += (0.05 * (levitation.getAmplifier() + 1) - movedVelocity.y) * 0.2; + } else if (!sim.player.level().isClientSide() || sim.player.level().hasChunkAt(blockPos)) { + y -= getEffectiveGravity(sim); + } else if (sim.pos.y > sim.player.level().getMinY()) { + y = -0.1; + } else { + y = 0.0; + } + + if (sim.player.shouldDiscardFriction()) { + sim.velocity = new Vec3(movedVelocity.x, y, movedVelocity.z); + } else { + sim.velocity = new Vec3(movedVelocity.x * friction, y * 0.9800000190734863, movedVelocity.z * friction); + } + } + + static void travelGliding(PlayerMovementSimulation sim, Vec3 movementInput) { + if (SneakAndClimbHelper.isClimbing(sim)) { + travelMidAir(sim, movementInput); + sim.gliding = false; + return; + } + + Vec3 oldVelocity = sim.velocity; + double horizontalSpeed = oldVelocity.horizontalDistance(); + sim.velocity = calcGlidingVelocity(sim, oldVelocity); + SneakAndClimbHelper.move(sim, sim.velocity); + + if (!sim.player.level().isClientSide()) { + double newHorizontalSpeed = sim.velocity.horizontalDistance(); + if (sim.horizontalCollision && oldVelocity.horizontalDistance() - newHorizontalSpeed > 0.0) { + sim.gliding = false; + } + } + } + + static Vec3 calcGlidingVelocity(PlayerMovementSimulation sim, Vec3 oldVelocity) { + Vec3 rotation = getRotationVector(sim); + float pitchRadians = sim.pitch * Mth.DEG_TO_RAD; + double horizontalLook = Math.sqrt(rotation.x * rotation.x + rotation.z * rotation.z); + double horizontalVelocity = oldVelocity.horizontalDistance(); + double gravity = getEffectiveGravity(sim); + double lift = Mth.square(Math.cos(pitchRadians)); + + oldVelocity = oldVelocity.add(0.0, gravity * (-1.0 + lift * 0.75), 0.0); + if (oldVelocity.y < 0.0 && horizontalLook > 0.0) { + double dive = oldVelocity.y * -0.1 * lift; + oldVelocity = oldVelocity.add(rotation.x * dive / horizontalLook, dive, rotation.z * dive / horizontalLook); + } + + if (pitchRadians < 0.0f && horizontalLook > 0.0) { + double climb = horizontalVelocity * -Mth.sin(pitchRadians) * 0.04; + oldVelocity = oldVelocity.add(-rotation.x * climb / horizontalLook, climb * 3.2, -rotation.z * climb / horizontalLook); + } + + if (horizontalLook > 0.0) { + oldVelocity = oldVelocity.add( + (rotation.x / horizontalLook * horizontalVelocity - oldVelocity.x) * 0.1, + 0.0, + (rotation.z / horizontalLook * horizontalVelocity - oldVelocity.z) * 0.1 + ); + } + + return oldVelocity.multiply(0.99f, 0.98f, 0.99f); + } + + static float getMovementSpeed(PlayerMovementSimulation sim, float slipperiness) { + if (sim.onGround) { + return sim.player.getSpeed() * (0.21600002f / (slipperiness * slipperiness * slipperiness)); + } + + return sim.sprinting ? 0.025999999f : 0.02f; + } + + static void updateVelocity(PlayerMovementSimulation sim, float speed, Vec3 movementInput) { + Vec3 added = EntityInvoker.lovisual$movementInputToVelocity(movementInput, speed, sim.yaw); + sim.velocity = sim.velocity.add(added); + } + + static Vec3 applyMovementInput(PlayerMovementSimulation sim, Vec3 movementInput, float slipperiness) { + updateVelocity(sim, getMovementSpeed(sim, slipperiness), movementInput); + sim.velocity = SneakAndClimbHelper.applyClimbingSpeed(sim, sim.velocity); + sim.velocity = FluidPhysicsHelper.applyWebSpeed(sim, sim.velocity); + SneakAndClimbHelper.move(sim, SneakAndClimbHelper.adjustMovementForSneaking(sim, sim.velocity)); + Vec3 movedVelocity = sim.velocity; + if ((sim.horizontalCollision || sim.jumping) && SneakAndClimbHelper.isClimbing(sim)) { + movedVelocity = new Vec3(movedVelocity.x, 0.2, movedVelocity.z); + } + return movedVelocity; + } + + static double getEffectiveGravity(PlayerMovementSimulation sim) { + double gravity = sim.player.getGravity(); + if (sim.velocity.y <= 0.0 && StatusEffectAccess.has(sim.player, MobEffects.SLOW_FALLING)) { + return Math.min(gravity, 0.01); + } + return gravity; + } + + static Vec3 getRotationVector(PlayerMovementSimulation sim) { + return Vec3.directionFromRotation(sim.pitch, sim.yaw); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/simulation/remote/RemotePlayerObservation.java b/mod/src/main/java/dev/loki/lovisual/util/player/simulation/remote/RemotePlayerObservation.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/simulation/remote/RemotePlayerObservation.java rename to mod/src/main/java/dev/loki/lovisual/util/player/simulation/remote/RemotePlayerObservation.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/simulation/remote/RemotePlayerSimulation.java b/mod/src/main/java/dev/loki/lovisual/util/player/simulation/remote/RemotePlayerSimulation.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/simulation/remote/RemotePlayerSimulation.java rename to mod/src/main/java/dev/loki/lovisual/util/player/simulation/remote/RemotePlayerSimulation.java diff --git a/mod/src/main/java/dev/loki/lovisual/util/player/simulation/state/FluidStateHelper.java b/mod/src/main/java/dev/loki/lovisual/util/player/simulation/state/FluidStateHelper.java new file mode 100644 index 0000000..077db6e --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/util/player/simulation/state/FluidStateHelper.java @@ -0,0 +1,134 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.util.player.simulation; + +import net.minecraft.core.BlockPos; +import net.minecraft.tags.FluidTags; +import net.minecraft.util.Mth; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; + +final class FluidStateHelper { + + private FluidStateHelper() { + } + + static void updateWaterState(PlayerMovementSimulation sim) { + sim.waterHeight = 0.0; + sim.lavaHeight = 0.0; + sim.touchingWater = updateMovementInFluid(sim, true, 0.014); + updateMovementInFluid(sim, false, 0.0023333333333333335); + } + + static void updateSubmergedInWaterState(PlayerMovementSimulation sim) { + double eyeY = sim.pos.y + sim.player.getEyeHeight(sim.player.getPose()); + BlockPos blockPos = BlockPos.containing(sim.pos.x, eyeY, sim.pos.z); + FluidState fluidState = sim.player.level().getFluidState(blockPos); + double fluidY = blockPos.getY() + fluidState.getHeight(sim.player.level(), blockPos); + sim.submergedInWater = fluidState.is(FluidTags.WATER) && fluidY > eyeY; + } + + static void updateSwimming(PlayerMovementSimulation sim) { + if (sim.player.getAbilities().flying) { + sim.swimming = false; + return; + } + + if (sim.swimming) { + sim.swimming = sim.sprinting && sim.touchingWater && !sim.player.isPassenger(); + } else { + FluidState state = sim.player.level().getFluidState(BlockPos.containing(sim.pos)); + sim.swimming = sim.sprinting && sim.submergedInWater && !sim.player.isPassenger() && state.is(FluidTags.WATER); + } + } + + static boolean updateMovementInFluid(PlayerMovementSimulation sim, boolean water, double speed) { + Level world = sim.player.level(); + if (isRegionUnloaded(sim, world)) { + return false; + } + + AABB box = sim.boundingBox.deflate(0.001); + int minX = Mth.floor(box.minX); + int maxX = Mth.ceil(box.maxX); + int minY = Mth.floor(box.minY); + int maxY = Mth.ceil(box.maxY); + int minZ = Mth.floor(box.minZ); + int maxZ = Mth.ceil(box.maxZ); + + double maxHeight = 0.0; + boolean found = false; + Vec3 addedVelocity = Vec3.ZERO; + int samples = 0; + BlockPos.MutableBlockPos mutable = new BlockPos.MutableBlockPos(); + + for (int x = minX; x < maxX; x++) { + for (int y = minY; y < maxY; y++) { + for (int z = minZ; z < maxZ; z++) { + mutable.set(x, y, z); + FluidState fluidState = world.getFluidState(mutable); + if (!(water ? fluidState.is(FluidTags.WATER) : fluidState.is(FluidTags.LAVA))) { + continue; + } + + double height = y + fluidState.getHeight(world, mutable); + if (height < box.minY) { + continue; + } + + found = true; + maxHeight = Math.max(height - box.minY, maxHeight); + Vec3 fluidVelocity = fluidState.getFlow(world, mutable); + if (maxHeight < 0.4) { + fluidVelocity = fluidVelocity.scale(maxHeight); + } + addedVelocity = addedVelocity.add(fluidVelocity); + samples++; + } + } + } + + if (samples > 0) { + addedVelocity = addedVelocity.scale(1.0 / samples).scale(speed); + if (Math.abs(sim.velocity.x) < 0.003 && Math.abs(sim.velocity.z) < 0.003 && addedVelocity.length() < 0.0045) { + addedVelocity = addedVelocity.normalize().scale(0.0045); + } + if (addedVelocity.lengthSqr() > 0.0) { + sim.velocity = sim.velocity.add(addedVelocity); + } + } + + if (water) { + sim.waterHeight = maxHeight; + } else { + sim.lavaHeight = maxHeight; + } + return found; + } + + private static boolean isRegionUnloaded(PlayerMovementSimulation sim, Level world) { + AABB box = sim.boundingBox.inflate(1.0); + int minX = Mth.floor(box.minX); + int maxX = Mth.ceil(box.maxX); + int minZ = Mth.floor(box.minZ); + int maxZ = Mth.ceil(box.maxZ); + + for (int x = minX >> 4; x <= (maxX - 1) >> 4; x++) { + for (int z = minZ >> 4; z <= (maxZ - 1) >> 4; z++) { + LevelChunk chunk = world.getChunkSource().getChunkNow(x, z); + if (chunk == null) { + return true; + } + } + } + return false; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/util/player/simulation/state/SneakAndClimbHelper.java b/mod/src/main/java/dev/loki/lovisual/util/player/simulation/state/SneakAndClimbHelper.java new file mode 100644 index 0000000..48d44a2 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/util/player/simulation/state/SneakAndClimbHelper.java @@ -0,0 +1,194 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.util.player.simulation; + +import net.minecraft.core.BlockPos; +import net.minecraft.tags.BlockTags; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityDimensions; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.BubbleColumnBlock; +import net.minecraft.world.level.block.LadderBlock; +import net.minecraft.world.level.block.TrapDoorBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.Vec3; + +import java.util.List; + +final class SneakAndClimbHelper { + + private SneakAndClimbHelper() { + } + + static Vec3 adjustMovementForSneaking(PlayerMovementSimulation sim, Vec3 movement) { + if (movement.y > 0.0 || !shouldConsiderLedgeClipping(sim)) { + return movement; + } + + double x = movement.x; + double z = movement.z; + while (x != 0.0 && sim.player.level().noCollision(sim.player, sim.boundingBox.move(x, -PlayerMovementSimulation.STEP_HEIGHT, 0.0))) { + if (x < 0.05 && x >= -0.05) { + x = 0.0; + } else if (x > 0.0) { + x -= 0.05; + } else { + x += 0.05; + } + } + + while (z != 0.0 && sim.player.level().noCollision(sim.player, sim.boundingBox.move(0.0, -PlayerMovementSimulation.STEP_HEIGHT, z))) { + if (z < 0.05 && z >= -0.05) { + z = 0.0; + } else if (z > 0.0) { + z -= 0.05; + } else { + z += 0.05; + } + } + + while (x != 0.0 && z != 0.0 && sim.player.level().noCollision(sim.player, sim.boundingBox.move(x, -PlayerMovementSimulation.STEP_HEIGHT, z))) { + if (x < 0.05 && x >= -0.05) { + x = 0.0; + } else if (x > 0.0) { + x -= 0.05; + } else { + x += 0.05; + } + + if (z < 0.05 && z >= -0.05) { + z = 0.0; + } else if (z > 0.0) { + z -= 0.05; + } else { + z += 0.05; + } + } + + if (movement.x != x || movement.z != z) { + sim.clipLedged = true; + } + + if (shouldClipAtLedge(sim)) { + return new Vec3(x, movement.y, z); + } + + return movement; + } + + static boolean shouldClipAtLedge(PlayerMovementSimulation sim) { + return !sim.input.ignoreClippingAtLedge && (sim.input.sneak || sim.input.forceSafeWalk); + } + + static boolean shouldConsiderLedgeClipping(PlayerMovementSimulation sim) { + return sim.onGround || sim.fallDistance < PlayerMovementSimulation.STEP_HEIGHT && !sim.player.level().noCollision( + sim.player, + sim.boundingBox.move(0.0, sim.fallDistance - PlayerMovementSimulation.STEP_HEIGHT, 0.0) + ); + } + + static Vec3 applyClimbingSpeed(PlayerMovementSimulation sim, Vec3 motion) { + if (!isClimbing(sim)) { + return motion; + } + + double x = Mth.clamp(motion.x, -0.15f, 0.15f); + double z = Mth.clamp(motion.z, -0.15f, 0.15f); + double y = Math.max(motion.y, -0.15f); + if (y < 0.0 && sim.player.isShiftKeyDown()) { + y = 0.0; + } + return new Vec3(x, y, z); + } + + static boolean isClimbing(PlayerMovementSimulation sim) { + if (sim.player.getAbilities().flying || sim.player.isSpectator()) { + return false; + } + + BlockPos blockPos = BlockPos.containing(sim.pos); + BlockState blockState = sim.player.level().getBlockState(blockPos); + if (sim.gliding && blockState.is(BlockTags.CAN_GLIDE_THROUGH)) { + return false; + } + if (blockState.is(BlockTags.CLIMBABLE)) { + return true; + } + return blockState.getBlock() instanceof TrapDoorBlock && canEnterTrapdoor(sim, blockPos, blockState); + } + + static float getVelocityMultiplier(PlayerMovementSimulation sim) { + if (sim.player.getAbilities().flying || sim.gliding) { + return 1.0f; + } + + BlockState state = sim.player.level().getBlockState(BlockPos.containing(sim.pos)); + float multiplier = state.getBlock().getSpeedFactor(); + if (!state.is(Blocks.WATER) && !state.is(Blocks.BUBBLE_COLUMN)) { + if (multiplier == 1.0f) { + multiplier = sim.player.level().getBlockState(sim.getVelocityAffectingPos()).getBlock().getSpeedFactor(); + } + } + return multiplier; + } + + private static boolean canEnterTrapdoor(PlayerMovementSimulation sim, BlockPos pos, BlockState state) { + if (!(Boolean) state.getValue(TrapDoorBlock.OPEN)) { + return false; + } + + BlockState belowState = sim.player.level().getBlockState(pos.below()); + return belowState.is(Blocks.LADDER) && belowState.getValue(LadderBlock.FACING) == state.getValue(TrapDoorBlock.FACING); + } + + static void move(PlayerMovementSimulation sim, Vec3 movement) { + Vec3 adjusted = movement.lengthSqr() == 0.0 + ? movement + : Entity.collideBoundingBox(sim.player, movement, sim.boundingBox, sim.player.level(), List.of()); + if (adjusted.lengthSqr() > 1.0E-7 || movement.lengthSqr() - adjusted.lengthSqr() < 1.0E-7) { + sim.pos = sim.pos.add(adjusted); + EntityDimensions dimensions = sim.player.getDimensions(sim.player.getPose()); + sim.boundingBox = dimensions.makeBoundingBox(sim.pos); + } + boolean xCollision = movement.x != adjusted.x; + boolean yCollision = movement.y != adjusted.y; + boolean zCollision = movement.z != adjusted.z; + sim.horizontalCollision = xCollision || zCollision; + sim.onGround = yCollision && movement.y < 0.0; + if (sim.onGround) { + sim.fallDistance = 0.0; + } else if (adjusted.y < 0.0) { + sim.fallDistance -= adjusted.y; + } + if (xCollision || zCollision || yCollision) { + sim.velocity = new Vec3(xCollision ? 0.0 : sim.velocity.x, yCollision ? 0.0 : sim.velocity.y, zCollision ? 0.0 : sim.velocity.z); + } + float velocityMultiplier = getVelocityMultiplier(sim); + sim.velocity = new Vec3(sim.velocity.x * velocityMultiplier, sim.velocity.y, sim.velocity.z * velocityMultiplier); + } + + static void applyBubbleColumnEffects(PlayerMovementSimulation sim) { + BlockPos pos = BlockPos.containing(sim.pos); + BlockState state = sim.player.level().getBlockState(pos); + if (!state.is(Blocks.BUBBLE_COLUMN)) { + return; + } + BlockState above = sim.player.level().getBlockState(pos.above()); + boolean surface = above.getCollisionShape(sim.player.level(), pos).isEmpty() && above.getFluidState().isEmpty(); + boolean drag = state.getValue(BubbleColumnBlock.DRAG_DOWN); + double y; + if (surface) { + y = drag ? Math.max(-0.9, sim.velocity.y - 0.03) : Math.min(1.8, sim.velocity.y + 0.1); + } else { + y = drag ? Math.max(-0.3, sim.velocity.y - 0.03) : Math.min(0.7, sim.velocity.y + 0.06); + sim.onGround = true; + } + sim.velocity = new Vec3(sim.velocity.x, y, sim.velocity.z); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/projectile/calc/CydhranianProjectileAngleCalculator.java b/mod/src/main/java/dev/loki/lovisual/util/projectile/calc/CydhranianProjectileAngleCalculator.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/projectile/calc/CydhranianProjectileAngleCalculator.java rename to mod/src/main/java/dev/loki/lovisual/util/projectile/calc/CydhranianProjectileAngleCalculator.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/projectile/calc/PolynomialProjectileAngleCalculator.java b/mod/src/main/java/dev/loki/lovisual/util/projectile/calc/PolynomialProjectileAngleCalculator.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/projectile/calc/PolynomialProjectileAngleCalculator.java rename to mod/src/main/java/dev/loki/lovisual/util/projectile/calc/PolynomialProjectileAngleCalculator.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/projectile/calc/ProjectileAngleCalculator.java b/mod/src/main/java/dev/loki/lovisual/util/projectile/calc/ProjectileAngleCalculator.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/projectile/calc/ProjectileAngleCalculator.java rename to mod/src/main/java/dev/loki/lovisual/util/projectile/calc/ProjectileAngleCalculator.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/projectile/calc/SituationalProjectileAngleCalculator.java b/mod/src/main/java/dev/loki/lovisual/util/projectile/calc/SituationalProjectileAngleCalculator.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/projectile/calc/SituationalProjectileAngleCalculator.java rename to mod/src/main/java/dev/loki/lovisual/util/projectile/calc/SituationalProjectileAngleCalculator.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/projectile/math/ProjectileMath.java b/mod/src/main/java/dev/loki/lovisual/util/projectile/math/ProjectileMath.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/projectile/math/ProjectileMath.java rename to mod/src/main/java/dev/loki/lovisual/util/projectile/math/ProjectileMath.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/projectile/math/ProjectilePredictionUtil.java b/mod/src/main/java/dev/loki/lovisual/util/projectile/math/ProjectilePredictionUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/projectile/math/ProjectilePredictionUtil.java rename to mod/src/main/java/dev/loki/lovisual/util/projectile/math/ProjectilePredictionUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/projectile/targeting/PointFinding.java b/mod/src/main/java/dev/loki/lovisual/util/projectile/targeting/PointFinding.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/projectile/targeting/PointFinding.java rename to mod/src/main/java/dev/loki/lovisual/util/projectile/targeting/PointFinding.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/projectile/targeting/ProjectileTarget.java b/mod/src/main/java/dev/loki/lovisual/util/projectile/targeting/ProjectileTarget.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/projectile/targeting/ProjectileTarget.java rename to mod/src/main/java/dev/loki/lovisual/util/projectile/targeting/ProjectileTarget.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/projectile/targeting/ProjectileTargetPointFinder.java b/mod/src/main/java/dev/loki/lovisual/util/projectile/targeting/ProjectileTargetPointFinder.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/projectile/targeting/ProjectileTargetPointFinder.java rename to mod/src/main/java/dev/loki/lovisual/util/projectile/targeting/ProjectileTargetPointFinder.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/projectile/trajectory/SimulatedArrow.java b/mod/src/main/java/dev/loki/lovisual/util/projectile/trajectory/SimulatedArrow.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/projectile/trajectory/SimulatedArrow.java rename to mod/src/main/java/dev/loki/lovisual/util/projectile/trajectory/SimulatedArrow.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/projectile/trajectory/TrajectoryData.java b/mod/src/main/java/dev/loki/lovisual/util/projectile/trajectory/TrajectoryData.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/projectile/trajectory/TrajectoryData.java rename to mod/src/main/java/dev/loki/lovisual/util/projectile/trajectory/TrajectoryData.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/projectile/trajectory/TrajectoryInfo.java b/mod/src/main/java/dev/loki/lovisual/util/projectile/trajectory/TrajectoryInfo.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/projectile/trajectory/TrajectoryInfo.java rename to mod/src/main/java/dev/loki/lovisual/util/projectile/trajectory/TrajectoryInfo.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/projectile/trajectory/TrajectoryType.java b/mod/src/main/java/dev/loki/lovisual/util/projectile/trajectory/TrajectoryType.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/projectile/trajectory/TrajectoryType.java rename to mod/src/main/java/dev/loki/lovisual/util/projectile/trajectory/TrajectoryType.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/protection/NotifyThrottle.java b/mod/src/main/java/dev/loki/lovisual/util/protection/NotifyThrottle.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/protection/NotifyThrottle.java rename to mod/src/main/java/dev/loki/lovisual/util/protection/NotifyThrottle.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/protection/SlotLockPolicy.java b/mod/src/main/java/dev/loki/lovisual/util/protection/SlotLockPolicy.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/protection/SlotLockPolicy.java rename to mod/src/main/java/dev/loki/lovisual/util/protection/SlotLockPolicy.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/proxy/ProxyBackend.java b/mod/src/main/java/dev/loki/lovisual/util/proxy/ProxyBackend.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/proxy/ProxyBackend.java rename to mod/src/main/java/dev/loki/lovisual/util/proxy/ProxyBackend.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/proxy/ProxyHandshakeHandler.java b/mod/src/main/java/dev/loki/lovisual/util/proxy/ProxyHandshakeHandler.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/proxy/ProxyHandshakeHandler.java rename to mod/src/main/java/dev/loki/lovisual/util/proxy/ProxyHandshakeHandler.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/proxy/ProxyNettyInstaller.java b/mod/src/main/java/dev/loki/lovisual/util/proxy/ProxyNettyInstaller.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/proxy/ProxyNettyInstaller.java rename to mod/src/main/java/dev/loki/lovisual/util/proxy/ProxyNettyInstaller.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/proxy/model/ProxyConfig.java b/mod/src/main/java/dev/loki/lovisual/util/proxy/model/ProxyConfig.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/proxy/model/ProxyConfig.java rename to mod/src/main/java/dev/loki/lovisual/util/proxy/model/ProxyConfig.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/proxy/model/ProxyEntry.java b/mod/src/main/java/dev/loki/lovisual/util/proxy/model/ProxyEntry.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/proxy/model/ProxyEntry.java rename to mod/src/main/java/dev/loki/lovisual/util/proxy/model/ProxyEntry.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/proxy/model/ProxyType.java b/mod/src/main/java/dev/loki/lovisual/util/proxy/model/ProxyType.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/proxy/model/ProxyType.java rename to mod/src/main/java/dev/loki/lovisual/util/proxy/model/ProxyType.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/client/CooldownManager.java b/mod/src/main/java/dev/loki/lovisual/util/pvp/client/CooldownManager.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/client/CooldownManager.java rename to mod/src/main/java/dev/loki/lovisual/util/pvp/client/CooldownManager.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/client/CooldownsState.java b/mod/src/main/java/dev/loki/lovisual/util/pvp/client/CooldownsState.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/client/CooldownsState.java rename to mod/src/main/java/dev/loki/lovisual/util/pvp/client/CooldownsState.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/client/PendingUseTracker.java b/mod/src/main/java/dev/loki/lovisual/util/pvp/client/PendingUseTracker.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/client/PendingUseTracker.java rename to mod/src/main/java/dev/loki/lovisual/util/pvp/client/PendingUseTracker.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/cooldown/CooldownRegistry.java b/mod/src/main/java/dev/loki/lovisual/util/pvp/cooldown/CooldownRegistry.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/cooldown/CooldownRegistry.java rename to mod/src/main/java/dev/loki/lovisual/util/pvp/cooldown/CooldownRegistry.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/cooldown/ItemCooldownRuleProvider.java b/mod/src/main/java/dev/loki/lovisual/util/pvp/cooldown/ItemCooldownRuleProvider.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/cooldown/ItemCooldownRuleProvider.java rename to mod/src/main/java/dev/loki/lovisual/util/pvp/cooldown/ItemCooldownRuleProvider.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/cooldown/ItemCooldownSnapshot.java b/mod/src/main/java/dev/loki/lovisual/util/pvp/cooldown/ItemCooldownSnapshot.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/cooldown/ItemCooldownSnapshot.java rename to mod/src/main/java/dev/loki/lovisual/util/pvp/cooldown/ItemCooldownSnapshot.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/cooldown/ItemUseCooldowns.java b/mod/src/main/java/dev/loki/lovisual/util/pvp/cooldown/ItemUseCooldowns.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/cooldown/ItemUseCooldowns.java rename to mod/src/main/java/dev/loki/lovisual/util/pvp/cooldown/ItemUseCooldowns.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/opponents/OpponentCooldownManager.java b/mod/src/main/java/dev/loki/lovisual/util/pvp/opponents/OpponentCooldownManager.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/opponents/OpponentCooldownManager.java rename to mod/src/main/java/dev/loki/lovisual/util/pvp/opponents/OpponentCooldownManager.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/opponents/TotemPopCounter.java b/mod/src/main/java/dev/loki/lovisual/util/pvp/opponents/TotemPopCounter.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/opponents/TotemPopCounter.java rename to mod/src/main/java/dev/loki/lovisual/util/pvp/opponents/TotemPopCounter.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/opponents/TotemPopSnapshot.java b/mod/src/main/java/dev/loki/lovisual/util/pvp/opponents/TotemPopSnapshot.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/opponents/TotemPopSnapshot.java rename to mod/src/main/java/dev/loki/lovisual/util/pvp/opponents/TotemPopSnapshot.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/parser/PvpChatParser.java b/mod/src/main/java/dev/loki/lovisual/util/pvp/parser/PvpChatParser.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/parser/PvpChatParser.java rename to mod/src/main/java/dev/loki/lovisual/util/pvp/parser/PvpChatParser.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/parser/PvpOverlayParser.java b/mod/src/main/java/dev/loki/lovisual/util/pvp/parser/PvpOverlayParser.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/parser/PvpOverlayParser.java rename to mod/src/main/java/dev/loki/lovisual/util/pvp/parser/PvpOverlayParser.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/parser/PvpTabParser.java b/mod/src/main/java/dev/loki/lovisual/util/pvp/parser/PvpTabParser.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/parser/PvpTabParser.java rename to mod/src/main/java/dev/loki/lovisual/util/pvp/parser/PvpTabParser.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/parser/PvpTextPatternUtil.java b/mod/src/main/java/dev/loki/lovisual/util/pvp/parser/PvpTextPatternUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/parser/PvpTextPatternUtil.java rename to mod/src/main/java/dev/loki/lovisual/util/pvp/parser/PvpTextPatternUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/state/PvpState.java b/mod/src/main/java/dev/loki/lovisual/util/pvp/state/PvpState.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/state/PvpState.java rename to mod/src/main/java/dev/loki/lovisual/util/pvp/state/PvpState.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/state/PvpTagSource.java b/mod/src/main/java/dev/loki/lovisual/util/pvp/state/PvpTagSource.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/state/PvpTagSource.java rename to mod/src/main/java/dev/loki/lovisual/util/pvp/state/PvpTagSource.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/state/PvpTargetState.java b/mod/src/main/java/dev/loki/lovisual/util/pvp/state/PvpTargetState.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/state/PvpTargetState.java rename to mod/src/main/java/dev/loki/lovisual/util/pvp/state/PvpTargetState.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/tracker/PvpTargetTracker.java b/mod/src/main/java/dev/loki/lovisual/util/pvp/tracker/PvpTargetTracker.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/tracker/PvpTargetTracker.java rename to mod/src/main/java/dev/loki/lovisual/util/pvp/tracker/PvpTargetTracker.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/tracker/PvpTracker.java b/mod/src/main/java/dev/loki/lovisual/util/pvp/tracker/PvpTracker.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/tracker/PvpTracker.java rename to mod/src/main/java/dev/loki/lovisual/util/pvp/tracker/PvpTracker.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/raycast/RaycastUtil.java b/mod/src/main/java/dev/loki/lovisual/util/raycast/RaycastUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/raycast/RaycastUtil.java rename to mod/src/main/java/dev/loki/lovisual/util/raycast/RaycastUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/resources/RenderResourceReadiness.java b/mod/src/main/java/dev/loki/lovisual/util/resources/RenderResourceReadiness.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/resources/RenderResourceReadiness.java rename to mod/src/main/java/dev/loki/lovisual/util/resources/RenderResourceReadiness.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/resources/ResourceReloadHooks.java b/mod/src/main/java/dev/loki/lovisual/util/resources/ResourceReloadHooks.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/resources/ResourceReloadHooks.java rename to mod/src/main/java/dev/loki/lovisual/util/resources/ResourceReloadHooks.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/screen/ClientScreen.java b/mod/src/main/java/dev/loki/lovisual/util/screen/ClientScreen.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/screen/ClientScreen.java rename to mod/src/main/java/dev/loki/lovisual/util/screen/ClientScreen.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/screen/ScreenCatalog.java b/mod/src/main/java/dev/loki/lovisual/util/screen/ScreenCatalog.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/screen/ScreenCatalog.java rename to mod/src/main/java/dev/loki/lovisual/util/screen/ScreenCatalog.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/session/MicrosoftSessionResult.java b/mod/src/main/java/dev/loki/lovisual/util/session/MicrosoftSessionResult.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/session/MicrosoftSessionResult.java rename to mod/src/main/java/dev/loki/lovisual/util/session/MicrosoftSessionResult.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/session/MinecraftGameConfigHolder.java b/mod/src/main/java/dev/loki/lovisual/util/session/MinecraftGameConfigHolder.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/session/MinecraftGameConfigHolder.java rename to mod/src/main/java/dev/loki/lovisual/util/session/MinecraftGameConfigHolder.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/session/SessionChanger.java b/mod/src/main/java/dev/loki/lovisual/util/session/SessionChanger.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/session/SessionChanger.java rename to mod/src/main/java/dev/loki/lovisual/util/session/SessionChanger.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/session/SessionLoginData.java b/mod/src/main/java/dev/loki/lovisual/util/session/SessionLoginData.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/session/SessionLoginData.java rename to mod/src/main/java/dev/loki/lovisual/util/session/SessionLoginData.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/session/microsoft/MicrosoftJson.java b/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/MicrosoftJson.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/session/microsoft/MicrosoftJson.java rename to mod/src/main/java/dev/loki/lovisual/util/session/microsoft/MicrosoftJson.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/DeviceAuthPendingException.java b/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/DeviceAuthPendingException.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/DeviceAuthPendingException.java rename to mod/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/DeviceAuthPendingException.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/MicrosoftAuthException.java b/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/MicrosoftAuthException.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/MicrosoftAuthException.java rename to mod/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/MicrosoftAuthException.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/MicrosoftAuthService.java b/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/MicrosoftAuthService.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/MicrosoftAuthService.java rename to mod/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/MicrosoftAuthService.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/MicrosoftDeviceCode.java b/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/MicrosoftDeviceCode.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/MicrosoftDeviceCode.java rename to mod/src/main/java/dev/loki/lovisual/util/session/microsoft/auth/MicrosoftDeviceCode.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/session/microsoft/model/MicrosoftTokens.java b/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/model/MicrosoftTokens.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/session/microsoft/model/MicrosoftTokens.java rename to mod/src/main/java/dev/loki/lovisual/util/session/microsoft/model/MicrosoftTokens.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/session/microsoft/model/MinecraftProfile.java b/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/model/MinecraftProfile.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/session/microsoft/model/MinecraftProfile.java rename to mod/src/main/java/dev/loki/lovisual/util/session/microsoft/model/MinecraftProfile.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/session/microsoft/model/XboxToken.java b/mod/src/main/java/dev/loki/lovisual/util/session/microsoft/model/XboxToken.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/session/microsoft/model/XboxToken.java rename to mod/src/main/java/dev/loki/lovisual/util/session/microsoft/model/XboxToken.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/target/TargetManager.java b/mod/src/main/java/dev/loki/lovisual/util/target/TargetManager.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/target/TargetManager.java rename to mod/src/main/java/dev/loki/lovisual/util/target/TargetManager.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/target/TargetingUtil.java b/mod/src/main/java/dev/loki/lovisual/util/target/TargetingUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/target/TargetingUtil.java rename to mod/src/main/java/dev/loki/lovisual/util/target/TargetingUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/text/edit/ClipboardUtil.java b/mod/src/main/java/dev/loki/lovisual/util/text/edit/ClipboardUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/text/edit/ClipboardUtil.java rename to mod/src/main/java/dev/loki/lovisual/util/text/edit/ClipboardUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/text/edit/TextSelection.java b/mod/src/main/java/dev/loki/lovisual/util/text/edit/TextSelection.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/text/edit/TextSelection.java rename to mod/src/main/java/dev/loki/lovisual/util/text/edit/TextSelection.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/text/format/LegacyTextUtil.java b/mod/src/main/java/dev/loki/lovisual/util/text/format/LegacyTextUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/text/format/LegacyTextUtil.java rename to mod/src/main/java/dev/loki/lovisual/util/text/format/LegacyTextUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/text/format/TextJsonUtil.java b/mod/src/main/java/dev/loki/lovisual/util/text/format/TextJsonUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/text/format/TextJsonUtil.java rename to mod/src/main/java/dev/loki/lovisual/util/text/format/TextJsonUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/text/format/TextRenderUtil.java b/mod/src/main/java/dev/loki/lovisual/util/text/format/TextRenderUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/text/format/TextRenderUtil.java rename to mod/src/main/java/dev/loki/lovisual/util/text/format/TextRenderUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/text/mask/ChatNameUtil.java b/mod/src/main/java/dev/loki/lovisual/util/text/mask/ChatNameUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/text/mask/ChatNameUtil.java rename to mod/src/main/java/dev/loki/lovisual/util/text/mask/ChatNameUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/text/mask/NameMasker.java b/mod/src/main/java/dev/loki/lovisual/util/text/mask/NameMasker.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/text/mask/NameMasker.java rename to mod/src/main/java/dev/loki/lovisual/util/text/mask/NameMasker.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/text/mask/NameProtector.java b/mod/src/main/java/dev/loki/lovisual/util/text/mask/NameProtector.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/text/mask/NameProtector.java rename to mod/src/main/java/dev/loki/lovisual/util/text/mask/NameProtector.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/time/GameClock.java b/mod/src/main/java/dev/loki/lovisual/util/time/GameClock.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/time/GameClock.java rename to mod/src/main/java/dev/loki/lovisual/util/time/GameClock.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/time/Timer.java b/mod/src/main/java/dev/loki/lovisual/util/time/Timer.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/time/Timer.java rename to mod/src/main/java/dev/loki/lovisual/util/time/Timer.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/time/TimerController.java b/mod/src/main/java/dev/loki/lovisual/util/time/TimerController.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/time/TimerController.java rename to mod/src/main/java/dev/loki/lovisual/util/time/TimerController.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/wav/engine/CustomSoundEngine.java b/mod/src/main/java/dev/loki/lovisual/util/wav/engine/CustomSoundEngine.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/wav/engine/CustomSoundEngine.java rename to mod/src/main/java/dev/loki/lovisual/util/wav/engine/CustomSoundEngine.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/wav/engine/SourcePool.java b/mod/src/main/java/dev/loki/lovisual/util/wav/engine/SourcePool.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/wav/engine/SourcePool.java rename to mod/src/main/java/dev/loki/lovisual/util/wav/engine/SourcePool.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/wav/engine/WavSound.java b/mod/src/main/java/dev/loki/lovisual/util/wav/engine/WavSound.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/wav/engine/WavSound.java rename to mod/src/main/java/dev/loki/lovisual/util/wav/engine/WavSound.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/wav/loader/WavBufferCache.java b/mod/src/main/java/dev/loki/lovisual/util/wav/loader/WavBufferCache.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/wav/loader/WavBufferCache.java rename to mod/src/main/java/dev/loki/lovisual/util/wav/loader/WavBufferCache.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/wav/loader/WavData.java b/mod/src/main/java/dev/loki/lovisual/util/wav/loader/WavData.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/wav/loader/WavData.java rename to mod/src/main/java/dev/loki/lovisual/util/wav/loader/WavData.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/wav/loader/WavLoader.java b/mod/src/main/java/dev/loki/lovisual/util/wav/loader/WavLoader.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/wav/loader/WavLoader.java rename to mod/src/main/java/dev/loki/lovisual/util/wav/loader/WavLoader.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/wav/util/ClickGuiSounds.java b/mod/src/main/java/dev/loki/lovisual/util/wav/util/ClickGuiSounds.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/wav/util/ClickGuiSounds.java rename to mod/src/main/java/dev/loki/lovisual/util/wav/util/ClickGuiSounds.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/wav/util/WavDebugStats.java b/mod/src/main/java/dev/loki/lovisual/util/wav/util/WavDebugStats.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/wav/util/WavDebugStats.java rename to mod/src/main/java/dev/loki/lovisual/util/wav/util/WavDebugStats.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/wav/util/WavSoundHelper.java b/mod/src/main/java/dev/loki/lovisual/util/wav/util/WavSoundHelper.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/wav/util/WavSoundHelper.java rename to mod/src/main/java/dev/loki/lovisual/util/wav/util/WavSoundHelper.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/world/ExplosionDamageUtil.java b/mod/src/main/java/dev/loki/lovisual/util/world/ExplosionDamageUtil.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/world/ExplosionDamageUtil.java rename to mod/src/main/java/dev/loki/lovisual/util/world/ExplosionDamageUtil.java diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/world/GpsBearing.java b/mod/src/main/java/dev/loki/lovisual/util/world/GpsBearing.java similarity index 100% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/util/world/GpsBearing.java rename to mod/src/main/java/dev/loki/lovisual/util/world/GpsBearing.java diff --git a/combatant-client-26.2/src/main/native/mediainfo/CMakeLists.txt b/mod/src/main/native/mediainfo/CMakeLists.txt similarity index 100% rename from combatant-client-26.2/src/main/native/mediainfo/CMakeLists.txt rename to mod/src/main/native/mediainfo/CMakeLists.txt diff --git a/combatant-client-26.2/src/main/native/mediainfo/main.cpp b/mod/src/main/native/mediainfo/main.cpp similarity index 100% rename from combatant-client-26.2/src/main/native/mediainfo/main.cpp rename to mod/src/main/native/mediainfo/main.cpp diff --git a/combatant-client-26.2/src/main/native/mediainfo/main.h b/mod/src/main/native/mediainfo/main.h similarity index 100% rename from combatant-client-26.2/src/main/native/mediainfo/main.h rename to mod/src/main/native/mediainfo/main.h diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/comfortaa.ttf b/mod/src/main/resources/assets/lovisual/font/comfortaa.ttf similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/comfortaa.ttf rename to mod/src/main/resources/assets/lovisual/font/comfortaa.ttf diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/icons.ttf b/mod/src/main/resources/assets/lovisual/font/icons.ttf similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/icons.ttf rename to mod/src/main/resources/assets/lovisual/font/icons.ttf diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/icons2.ttf b/mod/src/main/resources/assets/lovisual/font/icons2.ttf similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/icons2.ttf rename to mod/src/main/resources/assets/lovisual/font/icons2.ttf diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/iconsnur.ttf b/mod/src/main/resources/assets/lovisual/font/iconsnur.ttf similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/iconsnur.ttf rename to mod/src/main/resources/assets/lovisual/font/iconsnur.ttf diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/inter_bold.ttf b/mod/src/main/resources/assets/lovisual/font/inter_bold.ttf similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/inter_bold.ttf rename to mod/src/main/resources/assets/lovisual/font/inter_bold.ttf diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/inter_medium.ttf b/mod/src/main/resources/assets/lovisual/font/inter_medium.ttf similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/inter_medium.ttf rename to mod/src/main/resources/assets/lovisual/font/inter_medium.ttf diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/inter_regular.ttf b/mod/src/main/resources/assets/lovisual/font/inter_regular.ttf similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/inter_regular.ttf rename to mod/src/main/resources/assets/lovisual/font/inter_regular.ttf diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/iosevka-bold.ttf b/mod/src/main/resources/assets/lovisual/font/iosevka-bold.ttf similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/iosevka-bold.ttf rename to mod/src/main/resources/assets/lovisual/font/iosevka-bold.ttf diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/iosevka-bolditalic.ttf b/mod/src/main/resources/assets/lovisual/font/iosevka-bolditalic.ttf similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/iosevka-bolditalic.ttf rename to mod/src/main/resources/assets/lovisual/font/iosevka-bolditalic.ttf diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/iosevka-light.ttf b/mod/src/main/resources/assets/lovisual/font/iosevka-light.ttf similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/iosevka-light.ttf rename to mod/src/main/resources/assets/lovisual/font/iosevka-light.ttf diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/iosevka-lightitalic.ttf b/mod/src/main/resources/assets/lovisual/font/iosevka-lightitalic.ttf similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/iosevka-lightitalic.ttf rename to mod/src/main/resources/assets/lovisual/font/iosevka-lightitalic.ttf diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/iosevka-medium.ttf b/mod/src/main/resources/assets/lovisual/font/iosevka-medium.ttf similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/iosevka-medium.ttf rename to mod/src/main/resources/assets/lovisual/font/iosevka-medium.ttf diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/iosevka-mediumitalic.ttf b/mod/src/main/resources/assets/lovisual/font/iosevka-mediumitalic.ttf similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/iosevka-mediumitalic.ttf rename to mod/src/main/resources/assets/lovisual/font/iosevka-mediumitalic.ttf diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/mediaplayer.ttf b/mod/src/main/resources/assets/lovisual/font/mediaplayer.ttf similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/mediaplayer.ttf rename to mod/src/main/resources/assets/lovisual/font/mediaplayer.ttf diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/monsterrat.ttf b/mod/src/main/resources/assets/lovisual/font/monsterrat.ttf similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/monsterrat.ttf rename to mod/src/main/resources/assets/lovisual/font/monsterrat.ttf diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/comfortaa.json b/mod/src/main/resources/assets/lovisual/font/msdf/comfortaa.json similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/comfortaa.json rename to mod/src/main/resources/assets/lovisual/font/msdf/comfortaa.json diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/comfortaa.png b/mod/src/main/resources/assets/lovisual/font/msdf/comfortaa.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/comfortaa.png rename to mod/src/main/resources/assets/lovisual/font/msdf/comfortaa.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/guiicons.json b/mod/src/main/resources/assets/lovisual/font/msdf/guiicons.json similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/guiicons.json rename to mod/src/main/resources/assets/lovisual/font/msdf/guiicons.json diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/guiicons.png b/mod/src/main/resources/assets/lovisual/font/msdf/guiicons.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/guiicons.png rename to mod/src/main/resources/assets/lovisual/font/msdf/guiicons.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/inter_bold.json b/mod/src/main/resources/assets/lovisual/font/msdf/inter_bold.json similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/inter_bold.json rename to mod/src/main/resources/assets/lovisual/font/msdf/inter_bold.json diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/inter_bold.png b/mod/src/main/resources/assets/lovisual/font/msdf/inter_bold.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/inter_bold.png rename to mod/src/main/resources/assets/lovisual/font/msdf/inter_bold.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/inter_medium.json b/mod/src/main/resources/assets/lovisual/font/msdf/inter_medium.json similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/inter_medium.json rename to mod/src/main/resources/assets/lovisual/font/msdf/inter_medium.json diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/inter_medium.png b/mod/src/main/resources/assets/lovisual/font/msdf/inter_medium.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/inter_medium.png rename to mod/src/main/resources/assets/lovisual/font/msdf/inter_medium.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/inter_regular.json b/mod/src/main/resources/assets/lovisual/font/msdf/inter_regular.json similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/inter_regular.json rename to mod/src/main/resources/assets/lovisual/font/msdf/inter_regular.json diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/inter_regular.png b/mod/src/main/resources/assets/lovisual/font/msdf/inter_regular.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/inter_regular.png rename to mod/src/main/resources/assets/lovisual/font/msdf/inter_regular.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/iosevka-bold.json b/mod/src/main/resources/assets/lovisual/font/msdf/iosevka-bold.json similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/iosevka-bold.json rename to mod/src/main/resources/assets/lovisual/font/msdf/iosevka-bold.json diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/iosevka-bold.png b/mod/src/main/resources/assets/lovisual/font/msdf/iosevka-bold.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/iosevka-bold.png rename to mod/src/main/resources/assets/lovisual/font/msdf/iosevka-bold.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/iosevka-bolditalic.json b/mod/src/main/resources/assets/lovisual/font/msdf/iosevka-bolditalic.json similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/iosevka-bolditalic.json rename to mod/src/main/resources/assets/lovisual/font/msdf/iosevka-bolditalic.json diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/iosevka-bolditalic.png b/mod/src/main/resources/assets/lovisual/font/msdf/iosevka-bolditalic.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/iosevka-bolditalic.png rename to mod/src/main/resources/assets/lovisual/font/msdf/iosevka-bolditalic.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/iosevka-medium.json b/mod/src/main/resources/assets/lovisual/font/msdf/iosevka-medium.json similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/iosevka-medium.json rename to mod/src/main/resources/assets/lovisual/font/msdf/iosevka-medium.json diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/iosevka-medium.png b/mod/src/main/resources/assets/lovisual/font/msdf/iosevka-medium.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/iosevka-medium.png rename to mod/src/main/resources/assets/lovisual/font/msdf/iosevka-medium.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/iosevka-mediumitalic.json b/mod/src/main/resources/assets/lovisual/font/msdf/iosevka-mediumitalic.json similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/iosevka-mediumitalic.json rename to mod/src/main/resources/assets/lovisual/font/msdf/iosevka-mediumitalic.json diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/iosevka-mediumitalic.png b/mod/src/main/resources/assets/lovisual/font/msdf/iosevka-mediumitalic.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/iosevka-mediumitalic.png rename to mod/src/main/resources/assets/lovisual/font/msdf/iosevka-mediumitalic.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/mainmenuicons.json b/mod/src/main/resources/assets/lovisual/font/msdf/mainmenuicons.json similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/mainmenuicons.json rename to mod/src/main/resources/assets/lovisual/font/msdf/mainmenuicons.json diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/mainmenuicons.png b/mod/src/main/resources/assets/lovisual/font/msdf/mainmenuicons.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/mainmenuicons.png rename to mod/src/main/resources/assets/lovisual/font/msdf/mainmenuicons.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/monsterrat.json b/mod/src/main/resources/assets/lovisual/font/msdf/monsterrat.json similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/monsterrat.json rename to mod/src/main/resources/assets/lovisual/font/msdf/monsterrat.json diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/monsterrat.png b/mod/src/main/resources/assets/lovisual/font/msdf/monsterrat.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/monsterrat.png rename to mod/src/main/resources/assets/lovisual/font/msdf/monsterrat.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/onest_bold.json b/mod/src/main/resources/assets/lovisual/font/msdf/onest_bold.json similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/onest_bold.json rename to mod/src/main/resources/assets/lovisual/font/msdf/onest_bold.json diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/onest_bold.png b/mod/src/main/resources/assets/lovisual/font/msdf/onest_bold.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/onest_bold.png rename to mod/src/main/resources/assets/lovisual/font/msdf/onest_bold.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/onest_light.json b/mod/src/main/resources/assets/lovisual/font/msdf/onest_light.json similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/onest_light.json rename to mod/src/main/resources/assets/lovisual/font/msdf/onest_light.json diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/onest_light.png b/mod/src/main/resources/assets/lovisual/font/msdf/onest_light.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/onest_light.png rename to mod/src/main/resources/assets/lovisual/font/msdf/onest_light.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/onest_medium.json b/mod/src/main/resources/assets/lovisual/font/msdf/onest_medium.json similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/onest_medium.json rename to mod/src/main/resources/assets/lovisual/font/msdf/onest_medium.json diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/onest_medium.png b/mod/src/main/resources/assets/lovisual/font/msdf/onest_medium.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/onest_medium.png rename to mod/src/main/resources/assets/lovisual/font/msdf/onest_medium.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/onest_regular.json b/mod/src/main/resources/assets/lovisual/font/msdf/onest_regular.json similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/onest_regular.json rename to mod/src/main/resources/assets/lovisual/font/msdf/onest_regular.json diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/onest_regular.png b/mod/src/main/resources/assets/lovisual/font/msdf/onest_regular.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/onest_regular.png rename to mod/src/main/resources/assets/lovisual/font/msdf/onest_regular.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/profont.json b/mod/src/main/resources/assets/lovisual/font/msdf/profont.json similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/profont.json rename to mod/src/main/resources/assets/lovisual/font/msdf/profont.json diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/profont.png b/mod/src/main/resources/assets/lovisual/font/msdf/profont.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/profont.png rename to mod/src/main/resources/assets/lovisual/font/msdf/profont.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/richicons.json b/mod/src/main/resources/assets/lovisual/font/msdf/richicons.json similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/richicons.json rename to mod/src/main/resources/assets/lovisual/font/msdf/richicons.json diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/richicons.png b/mod/src/main/resources/assets/lovisual/font/msdf/richicons.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/msdf/richicons.png rename to mod/src/main/resources/assets/lovisual/font/msdf/richicons.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/onest_bold.ttf b/mod/src/main/resources/assets/lovisual/font/onest_bold.ttf similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/onest_bold.ttf rename to mod/src/main/resources/assets/lovisual/font/onest_bold.ttf diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/onest_light.ttf b/mod/src/main/resources/assets/lovisual/font/onest_light.ttf similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/onest_light.ttf rename to mod/src/main/resources/assets/lovisual/font/onest_light.ttf diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/onest_medium.ttf b/mod/src/main/resources/assets/lovisual/font/onest_medium.ttf similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/onest_medium.ttf rename to mod/src/main/resources/assets/lovisual/font/onest_medium.ttf diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/onest_regular.ttf b/mod/src/main/resources/assets/lovisual/font/onest_regular.ttf similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/onest_regular.ttf rename to mod/src/main/resources/assets/lovisual/font/onest_regular.ttf diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/profont.ttf b/mod/src/main/resources/assets/lovisual/font/profont.ttf similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/profont.ttf rename to mod/src/main/resources/assets/lovisual/font/profont.ttf diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/vanilla_symbols.ttf b/mod/src/main/resources/assets/lovisual/font/vanilla_symbols.ttf similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/vanilla_symbols.ttf rename to mod/src/main/resources/assets/lovisual/font/vanilla_symbols.ttf diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/font/weather_icons.ttf b/mod/src/main/resources/assets/lovisual/font/weather_icons.ttf similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/font/weather_icons.ttf rename to mod/src/main/resources/assets/lovisual/font/weather_icons.ttf diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/lang/en_us.json b/mod/src/main/resources/assets/lovisual/lang/en_us.json similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/lang/en_us.json rename to mod/src/main/resources/assets/lovisual/lang/en_us.json diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/lang/ru_ru.json b/mod/src/main/resources/assets/lovisual/lang/ru_ru.json similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/lang/ru_ru.json rename to mod/src/main/resources/assets/lovisual/lang/ru_ru.json diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/arc_batch.frag b/mod/src/main/resources/assets/lovisual/shaders/arc_batch.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/arc_batch.frag rename to mod/src/main/resources/assets/lovisual/shaders/arc_batch.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/block_highlight_hologram.frag b/mod/src/main/resources/assets/lovisual/shaders/block_highlight_hologram.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/block_highlight_hologram.frag rename to mod/src/main/resources/assets/lovisual/shaders/block_highlight_hologram.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/block_highlight_scanlines.frag b/mod/src/main/resources/assets/lovisual/shaders/block_highlight_scanlines.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/block_highlight_scanlines.frag rename to mod/src/main/resources/assets/lovisual/shaders/block_highlight_scanlines.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/block_highlight_watercaustic.frag b/mod/src/main/resources/assets/lovisual/shaders/block_highlight_watercaustic.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/block_highlight_watercaustic.frag rename to mod/src/main/resources/assets/lovisual/shaders/block_highlight_watercaustic.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/chamfered_rect_batch.frag b/mod/src/main/resources/assets/lovisual/shaders/chamfered_rect_batch.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/chamfered_rect_batch.frag rename to mod/src/main/resources/assets/lovisual/shaders/chamfered_rect_batch.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/chamfered_rect_stroke_batch.frag b/mod/src/main/resources/assets/lovisual/shaders/chamfered_rect_stroke_batch.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/chamfered_rect_stroke_batch.frag rename to mod/src/main/resources/assets/lovisual/shaders/chamfered_rect_stroke_batch.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/circle_batch.frag b/mod/src/main/resources/assets/lovisual/shaders/circle_batch.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/circle_batch.frag rename to mod/src/main/resources/assets/lovisual/shaders/circle_batch.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/core/entity_dither.fsh b/mod/src/main/resources/assets/lovisual/shaders/core/entity_dither.fsh similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/core/entity_dither.fsh rename to mod/src/main/resources/assets/lovisual/shaders/core/entity_dither.fsh diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/core/entity_dither.vsh b/mod/src/main/resources/assets/lovisual/shaders/core/entity_dither.vsh similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/core/entity_dither.vsh rename to mod/src/main/resources/assets/lovisual/shaders/core/entity_dither.vsh diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/damage_tint.frag b/mod/src/main/resources/assets/lovisual/shaders/damage_tint.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/damage_tint.frag rename to mod/src/main/resources/assets/lovisual/shaders/damage_tint.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/damage_tint.vert b/mod/src/main/resources/assets/lovisual/shaders/damage_tint.vert similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/damage_tint.vert rename to mod/src/main/resources/assets/lovisual/shaders/damage_tint.vert diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/depth_of_field.frag b/mod/src/main/resources/assets/lovisual/shaders/depth_of_field.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/depth_of_field.frag rename to mod/src/main/resources/assets/lovisual/shaders/depth_of_field.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/gui_texture_lookup.frag b/mod/src/main/resources/assets/lovisual/shaders/gui_texture_lookup.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/gui_texture_lookup.frag rename to mod/src/main/resources/assets/lovisual/shaders/gui_texture_lookup.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/hand_chams_balatro.frag b/mod/src/main/resources/assets/lovisual/shaders/hand_chams_balatro.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/hand_chams_balatro.frag rename to mod/src/main/resources/assets/lovisual/shaders/hand_chams_balatro.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/hand_chams_blend.frag b/mod/src/main/resources/assets/lovisual/shaders/hand_chams_blend.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/hand_chams_blend.frag rename to mod/src/main/resources/assets/lovisual/shaders/hand_chams_blend.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/hand_chams_chroma.frag b/mod/src/main/resources/assets/lovisual/shaders/hand_chams_chroma.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/hand_chams_chroma.frag rename to mod/src/main/resources/assets/lovisual/shaders/hand_chams_chroma.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/hand_chams_glow.frag b/mod/src/main/resources/assets/lovisual/shaders/hand_chams_glow.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/hand_chams_glow.frag rename to mod/src/main/resources/assets/lovisual/shaders/hand_chams_glow.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/hand_chams_invert.frag b/mod/src/main/resources/assets/lovisual/shaders/hand_chams_invert.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/hand_chams_invert.frag rename to mod/src/main/resources/assets/lovisual/shaders/hand_chams_invert.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/hand_chams_snow.frag b/mod/src/main/resources/assets/lovisual/shaders/hand_chams_snow.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/hand_chams_snow.frag rename to mod/src/main/resources/assets/lovisual/shaders/hand_chams_snow.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/hand_chams_solid.frag b/mod/src/main/resources/assets/lovisual/shaders/hand_chams_solid.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/hand_chams_solid.frag rename to mod/src/main/resources/assets/lovisual/shaders/hand_chams_solid.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/hand_chams_stripes.frag b/mod/src/main/resources/assets/lovisual/shaders/hand_chams_stripes.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/hand_chams_stripes.frag rename to mod/src/main/resources/assets/lovisual/shaders/hand_chams_stripes.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/hand_glass.frag b/mod/src/main/resources/assets/lovisual/shaders/hand_glass.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/hand_glass.frag rename to mod/src/main/resources/assets/lovisual/shaders/hand_glass.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/hand_metallic.frag b/mod/src/main/resources/assets/lovisual/shaders/hand_metallic.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/hand_metallic.frag rename to mod/src/main/resources/assets/lovisual/shaders/hand_metallic.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/hand_noise.frag b/mod/src/main/resources/assets/lovisual/shaders/hand_noise.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/hand_noise.frag rename to mod/src/main/resources/assets/lovisual/shaders/hand_noise.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/hand_smoke.frag b/mod/src/main/resources/assets/lovisual/shaders/hand_smoke.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/hand_smoke.frag rename to mod/src/main/resources/assets/lovisual/shaders/hand_smoke.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/heat_fx.frag b/mod/src/main/resources/assets/lovisual/shaders/heat_fx.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/heat_fx.frag rename to mod/src/main/resources/assets/lovisual/shaders/heat_fx.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/iris-patches/complementary_reimagined/r5/common.patch b/mod/src/main/resources/assets/lovisual/shaders/iris-patches/complementary_reimagined/r5/common.patch similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/iris-patches/complementary_reimagined/r5/common.patch rename to mod/src/main/resources/assets/lovisual/shaders/iris-patches/complementary_reimagined/r5/common.patch diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/iris-patches/complementary_reimagined/r5/composite1.patch b/mod/src/main/resources/assets/lovisual/shaders/iris-patches/complementary_reimagined/r5/composite1.patch similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/iris-patches/complementary_reimagined/r5/composite1.patch rename to mod/src/main/resources/assets/lovisual/shaders/iris-patches/complementary_reimagined/r5/composite1.patch diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/iris-patches/complementary_reimagined/r5/composite4.patch b/mod/src/main/resources/assets/lovisual/shaders/iris-patches/complementary_reimagined/r5/composite4.patch similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/iris-patches/complementary_reimagined/r5/composite4.patch rename to mod/src/main/resources/assets/lovisual/shaders/iris-patches/complementary_reimagined/r5/composite4.patch diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/iris-patches/complementary_reimagined/r5/deferred1.patch b/mod/src/main/resources/assets/lovisual/shaders/iris-patches/complementary_reimagined/r5/deferred1.patch similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/iris-patches/complementary_reimagined/r5/deferred1.patch rename to mod/src/main/resources/assets/lovisual/shaders/iris-patches/complementary_reimagined/r5/deferred1.patch diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/iris-patches/complementary_reimagined/r5/fog.patch b/mod/src/main/resources/assets/lovisual/shaders/iris-patches/complementary_reimagined/r5/fog.patch similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/iris-patches/complementary_reimagined/r5/fog.patch rename to mod/src/main/resources/assets/lovisual/shaders/iris-patches/complementary_reimagined/r5/fog.patch diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/iris-patches/complementary_reimagined/r5/lighting.patch b/mod/src/main/resources/assets/lovisual/shaders/iris-patches/complementary_reimagined/r5/lighting.patch similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/iris-patches/complementary_reimagined/r5/lighting.patch rename to mod/src/main/resources/assets/lovisual/shaders/iris-patches/complementary_reimagined/r5/lighting.patch diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/iris-patches/complementary_reimagined/r5/manifest.json b/mod/src/main/resources/assets/lovisual/shaders/iris-patches/complementary_reimagined/r5/manifest.json similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/iris-patches/complementary_reimagined/r5/manifest.json rename to mod/src/main/resources/assets/lovisual/shaders/iris-patches/complementary_reimagined/r5/manifest.json diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/iris-patches/complementary_reimagined/r5/water.patch b/mod/src/main/resources/assets/lovisual/shaders/iris-patches/complementary_reimagined/r5/water.patch similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/iris-patches/complementary_reimagined/r5/water.patch rename to mod/src/main/resources/assets/lovisual/shaders/iris-patches/complementary_reimagined/r5/water.patch diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/iris-patches/index.json b/mod/src/main/resources/assets/lovisual/shaders/iris-patches/index.json similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/iris-patches/index.json rename to mod/src/main/resources/assets/lovisual/shaders/iris-patches/index.json diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/iris-patches/photon/v1_3b/c0_vl.patch b/mod/src/main/resources/assets/lovisual/shaders/iris-patches/photon/v1_3b/c0_vl.patch similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/iris-patches/photon/v1_3b/c0_vl.patch rename to mod/src/main/resources/assets/lovisual/shaders/iris-patches/photon/v1_3b/c0_vl.patch diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/iris-patches/photon/v1_3b/c1_blend_layers.patch b/mod/src/main/resources/assets/lovisual/shaders/iris-patches/photon/v1_3b/c1_blend_layers.patch similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/iris-patches/photon/v1_3b/c1_blend_layers.patch rename to mod/src/main/resources/assets/lovisual/shaders/iris-patches/photon/v1_3b/c1_blend_layers.patch diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/iris-patches/photon/v1_3b/lighting.patch b/mod/src/main/resources/assets/lovisual/shaders/iris-patches/photon/v1_3b/lighting.patch similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/iris-patches/photon/v1_3b/lighting.patch rename to mod/src/main/resources/assets/lovisual/shaders/iris-patches/photon/v1_3b/lighting.patch diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/iris-patches/photon/v1_3b/manifest.json b/mod/src/main/resources/assets/lovisual/shaders/iris-patches/photon/v1_3b/manifest.json similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/iris-patches/photon/v1_3b/manifest.json rename to mod/src/main/resources/assets/lovisual/shaders/iris-patches/photon/v1_3b/manifest.json diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/iris-patches/photon/v1_3b/motion_blur.patch b/mod/src/main/resources/assets/lovisual/shaders/iris-patches/photon/v1_3b/motion_blur.patch similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/iris-patches/photon/v1_3b/motion_blur.patch rename to mod/src/main/resources/assets/lovisual/shaders/iris-patches/photon/v1_3b/motion_blur.patch diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/kill_effect_portal.frag b/mod/src/main/resources/assets/lovisual/shaders/kill_effect_portal.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/kill_effect_portal.frag rename to mod/src/main/resources/assets/lovisual/shaders/kill_effect_portal.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/kill_effect_tornado.frag b/mod/src/main/resources/assets/lovisual/shaders/kill_effect_tornado.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/kill_effect_tornado.frag rename to mod/src/main/resources/assets/lovisual/shaders/kill_effect_tornado.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/menu_background_aurora.frag b/mod/src/main/resources/assets/lovisual/shaders/menu_background_aurora.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/menu_background_aurora.frag rename to mod/src/main/resources/assets/lovisual/shaders/menu_background_aurora.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/menu_background_waves.frag b/mod/src/main/resources/assets/lovisual/shaders/menu_background_waves.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/menu_background_waves.frag rename to mod/src/main/resources/assets/lovisual/shaders/menu_background_waves.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/metallic.frag b/mod/src/main/resources/assets/lovisual/shaders/metallic.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/metallic.frag rename to mod/src/main/resources/assets/lovisual/shaders/metallic.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/motion_blur.frag b/mod/src/main/resources/assets/lovisual/shaders/motion_blur.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/motion_blur.frag rename to mod/src/main/resources/assets/lovisual/shaders/motion_blur.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/orbiz_ring_batch.frag b/mod/src/main/resources/assets/lovisual/shaders/orbiz_ring_batch.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/orbiz_ring_batch.frag rename to mod/src/main/resources/assets/lovisual/shaders/orbiz_ring_batch.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/portal_rift.frag b/mod/src/main/resources/assets/lovisual/shaders/portal_rift.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/portal_rift.frag rename to mod/src/main/resources/assets/lovisual/shaders/portal_rift.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_color.frag b/mod/src/main/resources/assets/lovisual/shaders/pos_color.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_color.frag rename to mod/src/main/resources/assets/lovisual/shaders/pos_color.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_color.vert b/mod/src/main/resources/assets/lovisual/shaders/pos_color.vert similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_color.vert rename to mod/src/main/resources/assets/lovisual/shaders/pos_color.vert diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_color_fog.frag b/mod/src/main/resources/assets/lovisual/shaders/pos_color_fog.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_color_fog.frag rename to mod/src/main/resources/assets/lovisual/shaders/pos_color_fog.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_color_fog.vert b/mod/src/main/resources/assets/lovisual/shaders/pos_color_fog.vert similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_color_fog.vert rename to mod/src/main/resources/assets/lovisual/shaders/pos_color_fog.vert diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_color_rect_params.vert b/mod/src/main/resources/assets/lovisual/shaders/pos_color_rect_params.vert similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_color_rect_params.vert rename to mod/src/main/resources/assets/lovisual/shaders/pos_color_rect_params.vert diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_color_rect_params2.vert b/mod/src/main/resources/assets/lovisual/shaders/pos_color_rect_params2.vert similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_color_rect_params2.vert rename to mod/src/main/resources/assets/lovisual/shaders/pos_color_rect_params2.vert diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_color_skybox_shader.vert b/mod/src/main/resources/assets/lovisual/shaders/pos_color_skybox_shader.vert similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_color_skybox_shader.vert rename to mod/src/main/resources/assets/lovisual/shaders/pos_color_skybox_shader.vert diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_local_color_rect_params.vert b/mod/src/main/resources/assets/lovisual/shaders/pos_local_color_rect_params.vert similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_local_color_rect_params.vert rename to mod/src/main/resources/assets/lovisual/shaders/pos_local_color_rect_params.vert diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_local_color_rect_params2.vert b/mod/src/main/resources/assets/lovisual/shaders/pos_local_color_rect_params2.vert similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_local_color_rect_params2.vert rename to mod/src/main/resources/assets/lovisual/shaders/pos_local_color_rect_params2.vert diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_local_color_rect_params5.vert b/mod/src/main/resources/assets/lovisual/shaders/pos_local_color_rect_params5.vert similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_local_color_rect_params5.vert rename to mod/src/main/resources/assets/lovisual/shaders/pos_local_color_rect_params5.vert diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_local_color_rect_params5_generic.vert b/mod/src/main/resources/assets/lovisual/shaders/pos_local_color_rect_params5_generic.vert similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_local_color_rect_params5_generic.vert rename to mod/src/main/resources/assets/lovisual/shaders/pos_local_color_rect_params5_generic.vert diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_tex_color.frag b/mod/src/main/resources/assets/lovisual/shaders/pos_tex_color.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_tex_color.frag rename to mod/src/main/resources/assets/lovisual/shaders/pos_tex_color.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_tex_color.vert b/mod/src/main/resources/assets/lovisual/shaders/pos_tex_color.vert similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_tex_color.vert rename to mod/src/main/resources/assets/lovisual/shaders/pos_tex_color.vert diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_tex_color_params2.vert b/mod/src/main/resources/assets/lovisual/shaders/pos_tex_color_params2.vert similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_tex_color_params2.vert rename to mod/src/main/resources/assets/lovisual/shaders/pos_tex_color_params2.vert diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_tex_color_rect_params.vert b/mod/src/main/resources/assets/lovisual/shaders/pos_tex_color_rect_params.vert similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_tex_color_rect_params.vert rename to mod/src/main/resources/assets/lovisual/shaders/pos_tex_color_rect_params.vert diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_tex_color_rect_params2.vert b/mod/src/main/resources/assets/lovisual/shaders/pos_tex_color_rect_params2.vert similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_tex_color_rect_params2.vert rename to mod/src/main/resources/assets/lovisual/shaders/pos_tex_color_rect_params2.vert diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_tex_color_sky_fog.frag b/mod/src/main/resources/assets/lovisual/shaders/pos_tex_color_sky_fog.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_tex_color_sky_fog.frag rename to mod/src/main/resources/assets/lovisual/shaders/pos_tex_color_sky_fog.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_tex_color_sky_fog.vert b/mod/src/main/resources/assets/lovisual/shaders/pos_tex_color_sky_fog.vert similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_tex_color_sky_fog.vert rename to mod/src/main/resources/assets/lovisual/shaders/pos_tex_color_sky_fog.vert diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_tex_color_sky_fog_additive.frag b/mod/src/main/resources/assets/lovisual/shaders/pos_tex_color_sky_fog_additive.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_tex_color_sky_fog_additive.frag rename to mod/src/main/resources/assets/lovisual/shaders/pos_tex_color_sky_fog_additive.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_tex_color_tint.frag b/mod/src/main/resources/assets/lovisual/shaders/pos_tex_color_tint.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_tex_color_tint.frag rename to mod/src/main/resources/assets/lovisual/shaders/pos_tex_color_tint.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_tex_local_color_rect_params.vert b/mod/src/main/resources/assets/lovisual/shaders/pos_tex_local_color_rect_params.vert similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_tex_local_color_rect_params.vert rename to mod/src/main/resources/assets/lovisual/shaders/pos_tex_local_color_rect_params.vert diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_tex_local_color_rect_params2.vert b/mod/src/main/resources/assets/lovisual/shaders/pos_tex_local_color_rect_params2.vert similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/pos_tex_local_color_rect_params2.vert rename to mod/src/main/resources/assets/lovisual/shaders/pos_tex_local_color_rect_params2.vert diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/post_fx.frag b/mod/src/main/resources/assets/lovisual/shaders/post_fx.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/post_fx.frag rename to mod/src/main/resources/assets/lovisual/shaders/post_fx.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/radial_glow_batch.frag b/mod/src/main/resources/assets/lovisual/shaders/radial_glow_batch.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/radial_glow_batch.frag rename to mod/src/main/resources/assets/lovisual/shaders/radial_glow_batch.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/reimagined_skybox.frag b/mod/src/main/resources/assets/lovisual/shaders/reimagined_skybox.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/reimagined_skybox.frag rename to mod/src/main/resources/assets/lovisual/shaders/reimagined_skybox.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/rounded_rect_batch.frag b/mod/src/main/resources/assets/lovisual/shaders/rounded_rect_batch.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/rounded_rect_batch.frag rename to mod/src/main/resources/assets/lovisual/shaders/rounded_rect_batch.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/rounded_rect_corners_batch.frag b/mod/src/main/resources/assets/lovisual/shaders/rounded_rect_corners_batch.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/rounded_rect_corners_batch.frag rename to mod/src/main/resources/assets/lovisual/shaders/rounded_rect_corners_batch.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/rounded_rect_glow_batch.frag b/mod/src/main/resources/assets/lovisual/shaders/rounded_rect_glow_batch.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/rounded_rect_glow_batch.frag rename to mod/src/main/resources/assets/lovisual/shaders/rounded_rect_glow_batch.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/rounded_rect_shadow_batch.frag b/mod/src/main/resources/assets/lovisual/shaders/rounded_rect_shadow_batch.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/rounded_rect_shadow_batch.frag rename to mod/src/main/resources/assets/lovisual/shaders/rounded_rect_shadow_batch.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/rounded_rect_soft_shadow_batch.frag b/mod/src/main/resources/assets/lovisual/shaders/rounded_rect_soft_shadow_batch.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/rounded_rect_soft_shadow_batch.frag rename to mod/src/main/resources/assets/lovisual/shaders/rounded_rect_soft_shadow_batch.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/rounded_rect_stroke_angular_batch.frag b/mod/src/main/resources/assets/lovisual/shaders/rounded_rect_stroke_angular_batch.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/rounded_rect_stroke_angular_batch.frag rename to mod/src/main/resources/assets/lovisual/shaders/rounded_rect_stroke_angular_batch.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/rounded_rect_stroke_batch.frag b/mod/src/main/resources/assets/lovisual/shaders/rounded_rect_stroke_batch.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/rounded_rect_stroke_batch.frag rename to mod/src/main/resources/assets/lovisual/shaders/rounded_rect_stroke_batch.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/rounded_rect_stroke_corners_batch.frag b/mod/src/main/resources/assets/lovisual/shaders/rounded_rect_stroke_corners_batch.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/rounded_rect_stroke_corners_batch.frag rename to mod/src/main/resources/assets/lovisual/shaders/rounded_rect_stroke_corners_batch.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/rounded_rect_tex_batch.frag b/mod/src/main/resources/assets/lovisual/shaders/rounded_rect_tex_batch.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/rounded_rect_tex_batch.frag rename to mod/src/main/resources/assets/lovisual/shaders/rounded_rect_tex_batch.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/rounded_rect_tex_mask_batch.frag b/mod/src/main/resources/assets/lovisual/shaders/rounded_rect_tex_mask_batch.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/rounded_rect_tex_mask_batch.frag rename to mod/src/main/resources/assets/lovisual/shaders/rounded_rect_tex_mask_batch.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/shader_esp_gradient.frag b/mod/src/main/resources/assets/lovisual/shaders/shader_esp_gradient.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/shader_esp_gradient.frag rename to mod/src/main/resources/assets/lovisual/shaders/shader_esp_gradient.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/shader_esp_shadow.frag b/mod/src/main/resources/assets/lovisual/shaders/shader_esp_shadow.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/shader_esp_shadow.frag rename to mod/src/main/resources/assets/lovisual/shaders/shader_esp_shadow.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/shader_esp_smoke.frag b/mod/src/main/resources/assets/lovisual/shaders/shader_esp_smoke.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/shader_esp_smoke.frag rename to mod/src/main/resources/assets/lovisual/shaders/shader_esp_smoke.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/shader_sky_pulsar.frag b/mod/src/main/resources/assets/lovisual/shaders/shader_sky_pulsar.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/shader_sky_pulsar.frag rename to mod/src/main/resources/assets/lovisual/shaders/shader_sky_pulsar.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/shader_sky_thunder.frag b/mod/src/main/resources/assets/lovisual/shaders/shader_sky_thunder.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/shader_sky_thunder.frag rename to mod/src/main/resources/assets/lovisual/shaders/shader_sky_thunder.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/sky_shader_caustic.frag b/mod/src/main/resources/assets/lovisual/shaders/sky_shader_caustic.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/sky_shader_caustic.frag rename to mod/src/main/resources/assets/lovisual/shaders/sky_shader_caustic.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/sky_shader_nebula.frag b/mod/src/main/resources/assets/lovisual/shaders/sky_shader_nebula.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/sky_shader_nebula.frag rename to mod/src/main/resources/assets/lovisual/shaders/sky_shader_nebula.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/sky_shader_sky.frag b/mod/src/main/resources/assets/lovisual/shaders/sky_shader_sky.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/sky_shader_sky.frag rename to mod/src/main/resources/assets/lovisual/shaders/sky_shader_sky.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/sky_shader_starry.frag b/mod/src/main/resources/assets/lovisual/shaders/sky_shader_starry.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/sky_shader_starry.frag rename to mod/src/main/resources/assets/lovisual/shaders/sky_shader_starry.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/sky_shader_water.frag b/mod/src/main/resources/assets/lovisual/shaders/sky_shader_water.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/sky_shader_water.frag rename to mod/src/main/resources/assets/lovisual/shaders/sky_shader_water.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/sky_sun.frag b/mod/src/main/resources/assets/lovisual/shaders/sky_sun.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/sky_sun.frag rename to mod/src/main/resources/assets/lovisual/shaders/sky_sun.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/sleep_overlay.frag b/mod/src/main/resources/assets/lovisual/shaders/sleep_overlay.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/sleep_overlay.frag rename to mod/src/main/resources/assets/lovisual/shaders/sleep_overlay.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/sodium/block_layer_opaque.fsh b/mod/src/main/resources/assets/lovisual/shaders/sodium/block_layer_opaque.fsh similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/sodium/block_layer_opaque.fsh rename to mod/src/main/resources/assets/lovisual/shaders/sodium/block_layer_opaque.fsh diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/sodium/block_layer_opaque.vsh b/mod/src/main/resources/assets/lovisual/shaders/sodium/block_layer_opaque.vsh similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/sodium/block_layer_opaque.vsh rename to mod/src/main/resources/assets/lovisual/shaders/sodium/block_layer_opaque.vsh diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/svg_msdf.frag b/mod/src/main/resources/assets/lovisual/shaders/svg_msdf.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/svg_msdf.frag rename to mod/src/main/resources/assets/lovisual/shaders/svg_msdf.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/text.frag b/mod/src/main/resources/assets/lovisual/shaders/text.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/text.frag rename to mod/src/main/resources/assets/lovisual/shaders/text.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/text.vert b/mod/src/main/resources/assets/lovisual/shaders/text.vert similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/text.vert rename to mod/src/main/resources/assets/lovisual/shaders/text.vert diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/text_liquid_glass.frag b/mod/src/main/resources/assets/lovisual/shaders/text_liquid_glass.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/text_liquid_glass.frag rename to mod/src/main/resources/assets/lovisual/shaders/text_liquid_glass.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/text_liquid_glass_msdf.frag b/mod/src/main/resources/assets/lovisual/shaders/text_liquid_glass_msdf.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/text_liquid_glass_msdf.frag rename to mod/src/main/resources/assets/lovisual/shaders/text_liquid_glass_msdf.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/text_msdf.frag b/mod/src/main/resources/assets/lovisual/shaders/text_msdf.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/text_msdf.frag rename to mod/src/main/resources/assets/lovisual/shaders/text_msdf.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/ui_blur.frag b/mod/src/main/resources/assets/lovisual/shaders/ui_blur.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/ui_blur.frag rename to mod/src/main/resources/assets/lovisual/shaders/ui_blur.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/ui_blur_batch.frag b/mod/src/main/resources/assets/lovisual/shaders/ui_blur_batch.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/ui_blur_batch.frag rename to mod/src/main/resources/assets/lovisual/shaders/ui_blur_batch.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/ui_blur_batch_corners.frag b/mod/src/main/resources/assets/lovisual/shaders/ui_blur_batch_corners.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/ui_blur_batch_corners.frag rename to mod/src/main/resources/assets/lovisual/shaders/ui_blur_batch_corners.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/ui_liquid_glass_batch.frag b/mod/src/main/resources/assets/lovisual/shaders/ui_liquid_glass_batch.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/ui_liquid_glass_batch.frag rename to mod/src/main/resources/assets/lovisual/shaders/ui_liquid_glass_batch.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/ui_rounded_fill_smoke_batch.frag b/mod/src/main/resources/assets/lovisual/shaders/ui_rounded_fill_smoke_batch.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/ui_rounded_fill_smoke_batch.frag rename to mod/src/main/resources/assets/lovisual/shaders/ui_rounded_fill_smoke_batch.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/ui_shape_batch.frag b/mod/src/main/resources/assets/lovisual/shaders/ui_shape_batch.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/ui_shape_batch.frag rename to mod/src/main/resources/assets/lovisual/shaders/ui_shape_batch.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/wet_surface.frag b/mod/src/main/resources/assets/lovisual/shaders/wet_surface.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/wet_surface.frag rename to mod/src/main/resources/assets/lovisual/shaders/wet_surface.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/wide_line.vert b/mod/src/main/resources/assets/lovisual/shaders/wide_line.vert similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/wide_line.vert rename to mod/src/main/resources/assets/lovisual/shaders/wide_line.vert diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/world_decal_sdf.frag b/mod/src/main/resources/assets/lovisual/shaders/world_decal_sdf.frag similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/shaders/world_decal_sdf.frag rename to mod/src/main/resources/assets/lovisual/shaders/world_decal_sdf.frag diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/sounds/disable/disable1.wav b/mod/src/main/resources/assets/lovisual/sounds/disable/disable1.wav similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/sounds/disable/disable1.wav rename to mod/src/main/resources/assets/lovisual/sounds/disable/disable1.wav diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/sounds/disable/disable2.wav b/mod/src/main/resources/assets/lovisual/sounds/disable/disable2.wav similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/sounds/disable/disable2.wav rename to mod/src/main/resources/assets/lovisual/sounds/disable/disable2.wav diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/sounds/disable/disable3.wav b/mod/src/main/resources/assets/lovisual/sounds/disable/disable3.wav similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/sounds/disable/disable3.wav rename to mod/src/main/resources/assets/lovisual/sounds/disable/disable3.wav diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/sounds/enable/enable1.wav b/mod/src/main/resources/assets/lovisual/sounds/enable/enable1.wav similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/sounds/enable/enable1.wav rename to mod/src/main/resources/assets/lovisual/sounds/enable/enable1.wav diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/sounds/enable/enable2.wav b/mod/src/main/resources/assets/lovisual/sounds/enable/enable2.wav similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/sounds/enable/enable2.wav rename to mod/src/main/resources/assets/lovisual/sounds/enable/enable2.wav diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/sounds/enable/enable3.wav b/mod/src/main/resources/assets/lovisual/sounds/enable/enable3.wav similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/sounds/enable/enable3.wav rename to mod/src/main/resources/assets/lovisual/sounds/enable/enable3.wav diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/sounds/gui/guibinding.wav b/mod/src/main/resources/assets/lovisual/sounds/gui/guibinding.wav similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/sounds/gui/guibinding.wav rename to mod/src/main/resources/assets/lovisual/sounds/gui/guibinding.wav diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/sounds/gui/guibindingnull.wav b/mod/src/main/resources/assets/lovisual/sounds/gui/guibindingnull.wav similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/sounds/gui/guibindingnull.wav rename to mod/src/main/resources/assets/lovisual/sounds/gui/guibindingnull.wav diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/sounds/gui/guibindingstart.wav b/mod/src/main/resources/assets/lovisual/sounds/gui/guibindingstart.wav similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/sounds/gui/guibindingstart.wav rename to mod/src/main/resources/assets/lovisual/sounds/gui/guibindingstart.wav diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/sounds/gui/guibindreset.wav b/mod/src/main/resources/assets/lovisual/sounds/gui/guibindreset.wav similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/sounds/gui/guibindreset.wav rename to mod/src/main/resources/assets/lovisual/sounds/gui/guibindreset.wav diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/sounds/gui/guichangemode.wav b/mod/src/main/resources/assets/lovisual/sounds/gui/guichangemode.wav similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/sounds/gui/guichangemode.wav rename to mod/src/main/resources/assets/lovisual/sounds/gui/guichangemode.wav diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/sounds/gui/guimodulehover.wav b/mod/src/main/resources/assets/lovisual/sounds/gui/guimodulehover.wav similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/sounds/gui/guimodulehover.wav rename to mod/src/main/resources/assets/lovisual/sounds/gui/guimodulehover.wav diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/sounds/gui/guino.wav b/mod/src/main/resources/assets/lovisual/sounds/gui/guino.wav similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/sounds/gui/guino.wav rename to mod/src/main/resources/assets/lovisual/sounds/gui/guino.wav diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/sounds/gui/guiscroll.wav b/mod/src/main/resources/assets/lovisual/sounds/gui/guiscroll.wav similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/sounds/gui/guiscroll.wav rename to mod/src/main/resources/assets/lovisual/sounds/gui/guiscroll.wav diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/sounds/gui/guislidermove.wav b/mod/src/main/resources/assets/lovisual/sounds/gui/guislidermove.wav similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/sounds/gui/guislidermove.wav rename to mod/src/main/resources/assets/lovisual/sounds/gui/guislidermove.wav diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/sounds/gui/guiyes.wav b/mod/src/main/resources/assets/lovisual/sounds/gui/guiyes.wav similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/sounds/gui/guiyes.wav rename to mod/src/main/resources/assets/lovisual/sounds/gui/guiyes.wav diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/sounds/gui/moduleclose.wav b/mod/src/main/resources/assets/lovisual/sounds/gui/moduleclose.wav similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/sounds/gui/moduleclose.wav rename to mod/src/main/resources/assets/lovisual/sounds/gui/moduleclose.wav diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/sounds/gui/moduleonclose.wav b/mod/src/main/resources/assets/lovisual/sounds/gui/moduleonclose.wav similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/sounds/gui/moduleonclose.wav rename to mod/src/main/resources/assets/lovisual/sounds/gui/moduleonclose.wav diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/sounds/gui/moduleonopen.wav b/mod/src/main/resources/assets/lovisual/sounds/gui/moduleonopen.wav similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/sounds/gui/moduleonopen.wav rename to mod/src/main/resources/assets/lovisual/sounds/gui/moduleonopen.wav diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/sounds/gui/moduleopen.wav b/mod/src/main/resources/assets/lovisual/sounds/gui/moduleopen.wav similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/sounds/gui/moduleopen.wav rename to mod/src/main/resources/assets/lovisual/sounds/gui/moduleopen.wav diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/sounds/hitsounds/bell.wav b/mod/src/main/resources/assets/lovisual/sounds/hitsounds/bell.wav similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/sounds/hitsounds/bell.wav rename to mod/src/main/resources/assets/lovisual/sounds/hitsounds/bell.wav diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/sounds/hitsounds/bonk.wav b/mod/src/main/resources/assets/lovisual/sounds/hitsounds/bonk.wav similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/sounds/hitsounds/bonk.wav rename to mod/src/main/resources/assets/lovisual/sounds/hitsounds/bonk.wav diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/sounds/hitsounds/bubble.wav b/mod/src/main/resources/assets/lovisual/sounds/hitsounds/bubble.wav similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/sounds/hitsounds/bubble.wav rename to mod/src/main/resources/assets/lovisual/sounds/hitsounds/bubble.wav diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/sounds/hitsounds/crime.wav b/mod/src/main/resources/assets/lovisual/sounds/hitsounds/crime.wav similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/sounds/hitsounds/crime.wav rename to mod/src/main/resources/assets/lovisual/sounds/hitsounds/crime.wav diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/sounds/hitsounds/critical.wav b/mod/src/main/resources/assets/lovisual/sounds/hitsounds/critical.wav similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/sounds/hitsounds/critical.wav rename to mod/src/main/resources/assets/lovisual/sounds/hitsounds/critical.wav diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/sounds/misc/friendadd.wav b/mod/src/main/resources/assets/lovisual/sounds/misc/friendadd.wav similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/sounds/misc/friendadd.wav rename to mod/src/main/resources/assets/lovisual/sounds/misc/friendadd.wav diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/sounds/misc/friendremove.wav b/mod/src/main/resources/assets/lovisual/sounds/misc/friendremove.wav similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/sounds/misc/friendremove.wav rename to mod/src/main/resources/assets/lovisual/sounds/misc/friendremove.wav diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/sounds/misc/orthodox.wav b/mod/src/main/resources/assets/lovisual/sounds/misc/orthodox.wav similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/sounds/misc/orthodox.wav rename to mod/src/main/resources/assets/lovisual/sounds/misc/orthodox.wav diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/sounds/misc/warning.wav b/mod/src/main/resources/assets/lovisual/sounds/misc/warning.wav similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/sounds/misc/warning.wav rename to mod/src/main/resources/assets/lovisual/sounds/misc/warning.wav diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/.gitkeep b/mod/src/main/resources/assets/lovisual/svg/.gitkeep similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/.gitkeep rename to mod/src/main/resources/assets/lovisual/svg/.gitkeep diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/accessibility.svg b/mod/src/main/resources/assets/lovisual/svg/accessibility.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/accessibility.svg rename to mod/src/main/resources/assets/lovisual/svg/accessibility.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/arrow-up-from-line.svg b/mod/src/main/resources/assets/lovisual/svg/arrow-up-from-line.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/arrow-up-from-line.svg rename to mod/src/main/resources/assets/lovisual/svg/arrow-up-from-line.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/brain-cog.svg b/mod/src/main/resources/assets/lovisual/svg/brain-cog.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/brain-cog.svg rename to mod/src/main/resources/assets/lovisual/svg/brain-cog.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/brush.svg b/mod/src/main/resources/assets/lovisual/svg/brush.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/brush.svg rename to mod/src/main/resources/assets/lovisual/svg/brush.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/check.svg b/mod/src/main/resources/assets/lovisual/svg/check.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/check.svg rename to mod/src/main/resources/assets/lovisual/svg/check.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/clipboard.svg b/mod/src/main/resources/assets/lovisual/svg/clipboard.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/clipboard.svg rename to mod/src/main/resources/assets/lovisual/svg/clipboard.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/columns-3-cog.svg b/mod/src/main/resources/assets/lovisual/svg/columns-3-cog.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/columns-3-cog.svg rename to mod/src/main/resources/assets/lovisual/svg/columns-3-cog.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/cross.svg b/mod/src/main/resources/assets/lovisual/svg/cross.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/cross.svg rename to mod/src/main/resources/assets/lovisual/svg/cross.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/diff.svg b/mod/src/main/resources/assets/lovisual/svg/diff.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/diff.svg rename to mod/src/main/resources/assets/lovisual/svg/diff.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/download.svg b/mod/src/main/resources/assets/lovisual/svg/download.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/download.svg rename to mod/src/main/resources/assets/lovisual/svg/download.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/ellipsis.svg b/mod/src/main/resources/assets/lovisual/svg/ellipsis.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/ellipsis.svg rename to mod/src/main/resources/assets/lovisual/svg/ellipsis.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/external-link.svg b/mod/src/main/resources/assets/lovisual/svg/external-link.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/external-link.svg rename to mod/src/main/resources/assets/lovisual/svg/external-link.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/folder-archive.svg b/mod/src/main/resources/assets/lovisual/svg/folder-archive.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/folder-archive.svg rename to mod/src/main/resources/assets/lovisual/svg/folder-archive.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/folder-check.svg b/mod/src/main/resources/assets/lovisual/svg/folder-check.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/folder-check.svg rename to mod/src/main/resources/assets/lovisual/svg/folder-check.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/folder-clock.svg b/mod/src/main/resources/assets/lovisual/svg/folder-clock.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/folder-clock.svg rename to mod/src/main/resources/assets/lovisual/svg/folder-clock.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/folder-closed.svg b/mod/src/main/resources/assets/lovisual/svg/folder-closed.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/folder-closed.svg rename to mod/src/main/resources/assets/lovisual/svg/folder-closed.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/folder-cog.svg b/mod/src/main/resources/assets/lovisual/svg/folder-cog.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/folder-cog.svg rename to mod/src/main/resources/assets/lovisual/svg/folder-cog.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/folder-down.svg b/mod/src/main/resources/assets/lovisual/svg/folder-down.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/folder-down.svg rename to mod/src/main/resources/assets/lovisual/svg/folder-down.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/folder-open.svg b/mod/src/main/resources/assets/lovisual/svg/folder-open.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/folder-open.svg rename to mod/src/main/resources/assets/lovisual/svg/folder-open.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/folder-output.svg b/mod/src/main/resources/assets/lovisual/svg/folder-output.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/folder-output.svg rename to mod/src/main/resources/assets/lovisual/svg/folder-output.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/folder-pen.svg b/mod/src/main/resources/assets/lovisual/svg/folder-pen.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/folder-pen.svg rename to mod/src/main/resources/assets/lovisual/svg/folder-pen.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/folder-plus.svg b/mod/src/main/resources/assets/lovisual/svg/folder-plus.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/folder-plus.svg rename to mod/src/main/resources/assets/lovisual/svg/folder-plus.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/folder-sync.svg b/mod/src/main/resources/assets/lovisual/svg/folder-sync.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/folder-sync.svg rename to mod/src/main/resources/assets/lovisual/svg/folder-sync.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/folder-tree.svg b/mod/src/main/resources/assets/lovisual/svg/folder-tree.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/folder-tree.svg rename to mod/src/main/resources/assets/lovisual/svg/folder-tree.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/folder-x.svg b/mod/src/main/resources/assets/lovisual/svg/folder-x.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/folder-x.svg rename to mod/src/main/resources/assets/lovisual/svg/folder-x.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/folder.svg b/mod/src/main/resources/assets/lovisual/svg/folder.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/folder.svg rename to mod/src/main/resources/assets/lovisual/svg/folder.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/heart-plus.svg b/mod/src/main/resources/assets/lovisual/svg/heart-plus.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/heart-plus.svg rename to mod/src/main/resources/assets/lovisual/svg/heart-plus.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/heart-pulse.svg b/mod/src/main/resources/assets/lovisual/svg/heart-pulse.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/heart-pulse.svg rename to mod/src/main/resources/assets/lovisual/svg/heart-pulse.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/minus.svg b/mod/src/main/resources/assets/lovisual/svg/minus.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/minus.svg rename to mod/src/main/resources/assets/lovisual/svg/minus.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/package.svg b/mod/src/main/resources/assets/lovisual/svg/package.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/package.svg rename to mod/src/main/resources/assets/lovisual/svg/package.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/paintbrush.svg b/mod/src/main/resources/assets/lovisual/svg/paintbrush.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/paintbrush.svg rename to mod/src/main/resources/assets/lovisual/svg/paintbrush.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/palette.svg b/mod/src/main/resources/assets/lovisual/svg/palette.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/palette.svg rename to mod/src/main/resources/assets/lovisual/svg/palette.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/repeat-1.svg b/mod/src/main/resources/assets/lovisual/svg/repeat-1.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/repeat-1.svg rename to mod/src/main/resources/assets/lovisual/svg/repeat-1.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/repeat-off.svg b/mod/src/main/resources/assets/lovisual/svg/repeat-off.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/repeat-off.svg rename to mod/src/main/resources/assets/lovisual/svg/repeat-off.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/repeat.svg b/mod/src/main/resources/assets/lovisual/svg/repeat.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/repeat.svg rename to mod/src/main/resources/assets/lovisual/svg/repeat.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/save.svg b/mod/src/main/resources/assets/lovisual/svg/save.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/save.svg rename to mod/src/main/resources/assets/lovisual/svg/save.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/shield-user.svg b/mod/src/main/resources/assets/lovisual/svg/shield-user.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/shield-user.svg rename to mod/src/main/resources/assets/lovisual/svg/shield-user.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/swords.svg b/mod/src/main/resources/assets/lovisual/svg/swords.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/swords.svg rename to mod/src/main/resources/assets/lovisual/svg/swords.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/toggle-left.svg b/mod/src/main/resources/assets/lovisual/svg/toggle-left.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/toggle-left.svg rename to mod/src/main/resources/assets/lovisual/svg/toggle-left.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/toggle-right.svg b/mod/src/main/resources/assets/lovisual/svg/toggle-right.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/toggle-right.svg rename to mod/src/main/resources/assets/lovisual/svg/toggle-right.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/toggle-thumb.svg b/mod/src/main/resources/assets/lovisual/svg/toggle-thumb.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/toggle-thumb.svg rename to mod/src/main/resources/assets/lovisual/svg/toggle-thumb.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/toggle-track.svg b/mod/src/main/resources/assets/lovisual/svg/toggle-track.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/toggle-track.svg rename to mod/src/main/resources/assets/lovisual/svg/toggle-track.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/trash-2.svg b/mod/src/main/resources/assets/lovisual/svg/trash-2.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/trash-2.svg rename to mod/src/main/resources/assets/lovisual/svg/trash-2.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/tree-deciduous.svg b/mod/src/main/resources/assets/lovisual/svg/tree-deciduous.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/tree-deciduous.svg rename to mod/src/main/resources/assets/lovisual/svg/tree-deciduous.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/tree-palm.svg b/mod/src/main/resources/assets/lovisual/svg/tree-palm.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/tree-palm.svg rename to mod/src/main/resources/assets/lovisual/svg/tree-palm.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/tree-pine.svg b/mod/src/main/resources/assets/lovisual/svg/tree-pine.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/tree-pine.svg rename to mod/src/main/resources/assets/lovisual/svg/tree-pine.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/trees.svg b/mod/src/main/resources/assets/lovisual/svg/trees.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/trees.svg rename to mod/src/main/resources/assets/lovisual/svg/trees.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/user-check.svg b/mod/src/main/resources/assets/lovisual/svg/user-check.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/user-check.svg rename to mod/src/main/resources/assets/lovisual/svg/user-check.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/user-cog.svg b/mod/src/main/resources/assets/lovisual/svg/user-cog.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/user-cog.svg rename to mod/src/main/resources/assets/lovisual/svg/user-cog.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/user-minus.svg b/mod/src/main/resources/assets/lovisual/svg/user-minus.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/user-minus.svg rename to mod/src/main/resources/assets/lovisual/svg/user-minus.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/user-pen.svg b/mod/src/main/resources/assets/lovisual/svg/user-pen.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/user-pen.svg rename to mod/src/main/resources/assets/lovisual/svg/user-pen.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/user-plus.svg b/mod/src/main/resources/assets/lovisual/svg/user-plus.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/user-plus.svg rename to mod/src/main/resources/assets/lovisual/svg/user-plus.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/user.svg b/mod/src/main/resources/assets/lovisual/svg/user.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/user.svg rename to mod/src/main/resources/assets/lovisual/svg/user.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/vanilla/u1f30a.svg b/mod/src/main/resources/assets/lovisual/svg/vanilla/u1f30a.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/vanilla/u1f30a.svg rename to mod/src/main/resources/assets/lovisual/svg/vanilla/u1f30a.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/vanilla/u1f356.svg b/mod/src/main/resources/assets/lovisual/svg/vanilla/u1f356.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/vanilla/u1f356.svg rename to mod/src/main/resources/assets/lovisual/svg/vanilla/u1f356.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/vanilla/u1f3a3.svg b/mod/src/main/resources/assets/lovisual/svg/vanilla/u1f3a3.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/vanilla/u1f3a3.svg rename to mod/src/main/resources/assets/lovisual/svg/vanilla/u1f3a3.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/vanilla/u1f3f9.svg b/mod/src/main/resources/assets/lovisual/svg/vanilla/u1f3f9.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/vanilla/u1f3f9.svg rename to mod/src/main/resources/assets/lovisual/svg/vanilla/u1f3f9.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/vanilla/u1f514.svg b/mod/src/main/resources/assets/lovisual/svg/vanilla/u1f514.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/vanilla/u1f514.svg rename to mod/src/main/resources/assets/lovisual/svg/vanilla/u1f514.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/vanilla/u1f525.svg b/mod/src/main/resources/assets/lovisual/svg/vanilla/u1f525.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/vanilla/u1f525.svg rename to mod/src/main/resources/assets/lovisual/svg/vanilla/u1f525.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/vanilla/u1f531.svg b/mod/src/main/resources/assets/lovisual/svg/vanilla/u1f531.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/vanilla/u1f531.svg rename to mod/src/main/resources/assets/lovisual/svg/vanilla/u1f531.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/vanilla/u1f9ea.svg b/mod/src/main/resources/assets/lovisual/svg/vanilla/u1f9ea.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/vanilla/u1f9ea.svg rename to mod/src/main/resources/assets/lovisual/svg/vanilla/u1f9ea.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/vanilla/u274c.svg b/mod/src/main/resources/assets/lovisual/svg/vanilla/u274c.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/vanilla/u274c.svg rename to mod/src/main/resources/assets/lovisual/svg/vanilla/u274c.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/svg/x.svg b/mod/src/main/resources/assets/lovisual/svg/x.svg similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/svg/x.svg rename to mod/src/main/resources/assets/lovisual/svg/x.svg diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/bloom.png b/mod/src/main/resources/assets/lovisual/textures/bloom.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/bloom.png rename to mod/src/main/resources/assets/lovisual/textures/bloom.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/glow_particle.png b/mod/src/main/resources/assets/lovisual/textures/glow_particle.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/glow_particle.png rename to mod/src/main/resources/assets/lovisual/textures/glow_particle.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/hud/elements/arrow.png b/mod/src/main/resources/assets/lovisual/textures/hud/elements/arrow.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/hud/elements/arrow.png rename to mod/src/main/resources/assets/lovisual/textures/hud/elements/arrow.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/hud/elements/arrow2.png b/mod/src/main/resources/assets/lovisual/textures/hud/elements/arrow2.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/hud/elements/arrow2.png rename to mod/src/main/resources/assets/lovisual/textures/hud/elements/arrow2.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/hud/elements/arrow3.png b/mod/src/main/resources/assets/lovisual/textures/hud/elements/arrow3.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/hud/elements/arrow3.png rename to mod/src/main/resources/assets/lovisual/textures/hud/elements/arrow3.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/hud/elements/arrow4.png b/mod/src/main/resources/assets/lovisual/textures/hud/elements/arrow4.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/hud/elements/arrow4.png rename to mod/src/main/resources/assets/lovisual/textures/hud/elements/arrow4.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/hud/elements/capture.png b/mod/src/main/resources/assets/lovisual/textures/hud/elements/capture.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/hud/elements/capture.png rename to mod/src/main/resources/assets/lovisual/textures/hud/elements/capture.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/hud/elements/coords.png b/mod/src/main/resources/assets/lovisual/textures/hud/elements/coords.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/hud/elements/coords.png rename to mod/src/main/resources/assets/lovisual/textures/hud/elements/coords.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/hud/elements/fps.png b/mod/src/main/resources/assets/lovisual/textures/hud/elements/fps.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/hud/elements/fps.png rename to mod/src/main/resources/assets/lovisual/textures/hud/elements/fps.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/hud/elements/memory.png b/mod/src/main/resources/assets/lovisual/textures/hud/elements/memory.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/hud/elements/memory.png rename to mod/src/main/resources/assets/lovisual/textures/hud/elements/memory.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/hud/elements/ping.png b/mod/src/main/resources/assets/lovisual/textures/hud/elements/ping.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/hud/elements/ping.png rename to mod/src/main/resources/assets/lovisual/textures/hud/elements/ping.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/hud/elements/server.png b/mod/src/main/resources/assets/lovisual/textures/hud/elements/server.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/hud/elements/server.png rename to mod/src/main/resources/assets/lovisual/textures/hud/elements/server.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/hud/elements/speedometer.png b/mod/src/main/resources/assets/lovisual/textures/hud/elements/speedometer.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/hud/elements/speedometer.png rename to mod/src/main/resources/assets/lovisual/textures/hud/elements/speedometer.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/hud/elements/tps.png b/mod/src/main/resources/assets/lovisual/textures/hud/elements/tps.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/hud/elements/tps.png rename to mod/src/main/resources/assets/lovisual/textures/hud/elements/tps.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/bubble.png b/mod/src/main/resources/assets/lovisual/textures/particles/bubble.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/bubble.png rename to mod/src/main/resources/assets/lovisual/textures/particles/bubble.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/cross.png b/mod/src/main/resources/assets/lovisual/textures/particles/cross.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/cross.png rename to mod/src/main/resources/assets/lovisual/textures/particles/cross.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/cube.png b/mod/src/main/resources/assets/lovisual/textures/particles/cube.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/cube.png rename to mod/src/main/resources/assets/lovisual/textures/particles/cube.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/default_circle.png b/mod/src/main/resources/assets/lovisual/textures/particles/default_circle.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/default_circle.png rename to mod/src/main/resources/assets/lovisual/textures/particles/default_circle.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/feather.png b/mod/src/main/resources/assets/lovisual/textures/particles/feather.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/feather.png rename to mod/src/main/resources/assets/lovisual/textures/particles/feather.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/firefly.png b/mod/src/main/resources/assets/lovisual/textures/particles/firefly.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/firefly.png rename to mod/src/main/resources/assets/lovisual/textures/particles/firefly.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/funnel/distortion.png b/mod/src/main/resources/assets/lovisual/textures/particles/funnel/distortion.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/funnel/distortion.png rename to mod/src/main/resources/assets/lovisual/textures/particles/funnel/distortion.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/funnel/funnel_eye.png b/mod/src/main/resources/assets/lovisual/textures/particles/funnel/funnel_eye.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/funnel/funnel_eye.png rename to mod/src/main/resources/assets/lovisual/textures/particles/funnel/funnel_eye.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/glow.png b/mod/src/main/resources/assets/lovisual/textures/particles/glow.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/glow.png rename to mod/src/main/resources/assets/lovisual/textures/particles/glow.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/jump_circle.png b/mod/src/main/resources/assets/lovisual/textures/particles/jump_circle.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/jump_circle.png rename to mod/src/main/resources/assets/lovisual/textures/particles/jump_circle.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/mcross.png b/mod/src/main/resources/assets/lovisual/textures/particles/mcross.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/mcross.png rename to mod/src/main/resources/assets/lovisual/textures/particles/mcross.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/moon.png b/mod/src/main/resources/assets/lovisual/textures/particles/moon.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/moon.png rename to mod/src/main/resources/assets/lovisual/textures/particles/moon.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/p1.png b/mod/src/main/resources/assets/lovisual/textures/particles/p1.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/p1.png rename to mod/src/main/resources/assets/lovisual/textures/particles/p1.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/p10.png b/mod/src/main/resources/assets/lovisual/textures/particles/p10.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/p10.png rename to mod/src/main/resources/assets/lovisual/textures/particles/p10.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/p11.png b/mod/src/main/resources/assets/lovisual/textures/particles/p11.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/p11.png rename to mod/src/main/resources/assets/lovisual/textures/particles/p11.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/p12.png b/mod/src/main/resources/assets/lovisual/textures/particles/p12.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/p12.png rename to mod/src/main/resources/assets/lovisual/textures/particles/p12.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/p2.png b/mod/src/main/resources/assets/lovisual/textures/particles/p2.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/p2.png rename to mod/src/main/resources/assets/lovisual/textures/particles/p2.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/p3.png b/mod/src/main/resources/assets/lovisual/textures/particles/p3.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/p3.png rename to mod/src/main/resources/assets/lovisual/textures/particles/p3.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/p4.png b/mod/src/main/resources/assets/lovisual/textures/particles/p4.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/p4.png rename to mod/src/main/resources/assets/lovisual/textures/particles/p4.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/p5.png b/mod/src/main/resources/assets/lovisual/textures/particles/p5.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/p5.png rename to mod/src/main/resources/assets/lovisual/textures/particles/p5.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/p7.png b/mod/src/main/resources/assets/lovisual/textures/particles/p7.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/p7.png rename to mod/src/main/resources/assets/lovisual/textures/particles/p7.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/p8.png b/mod/src/main/resources/assets/lovisual/textures/particles/p8.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/p8.png rename to mod/src/main/resources/assets/lovisual/textures/particles/p8.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/p9.png b/mod/src/main/resources/assets/lovisual/textures/particles/p9.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/p9.png rename to mod/src/main/resources/assets/lovisual/textures/particles/p9.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/spark.png b/mod/src/main/resources/assets/lovisual/textures/particles/spark.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/spark.png rename to mod/src/main/resources/assets/lovisual/textures/particles/spark.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/star.png b/mod/src/main/resources/assets/lovisual/textures/particles/star.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/star.png rename to mod/src/main/resources/assets/lovisual/textures/particles/star.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/triangle.png b/mod/src/main/resources/assets/lovisual/textures/particles/triangle.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/particles/triangle.png rename to mod/src/main/resources/assets/lovisual/textures/particles/triangle.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/timers/arrow.png b/mod/src/main/resources/assets/lovisual/textures/timers/arrow.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/timers/arrow.png rename to mod/src/main/resources/assets/lovisual/textures/timers/arrow.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/timers/pearl.png b/mod/src/main/resources/assets/lovisual/textures/timers/pearl.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/timers/pearl.png rename to mod/src/main/resources/assets/lovisual/textures/timers/pearl.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/timers/potion.png b/mod/src/main/resources/assets/lovisual/textures/timers/potion.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/timers/potion.png rename to mod/src/main/resources/assets/lovisual/textures/timers/potion.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/timers/tnt.png b/mod/src/main/resources/assets/lovisual/textures/timers/tnt.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/timers/tnt.png rename to mod/src/main/resources/assets/lovisual/textures/timers/tnt.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/timers/trident.png b/mod/src/main/resources/assets/lovisual/textures/timers/trident.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/timers/trident.png rename to mod/src/main/resources/assets/lovisual/textures/timers/trident.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_1.png b/mod/src/main/resources/assets/lovisual/textures/trollface/trollface_1.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_1.png rename to mod/src/main/resources/assets/lovisual/textures/trollface/trollface_1.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_2.png b/mod/src/main/resources/assets/lovisual/textures/trollface/trollface_2.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_2.png rename to mod/src/main/resources/assets/lovisual/textures/trollface/trollface_2.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_3.png b/mod/src/main/resources/assets/lovisual/textures/trollface/trollface_3.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_3.png rename to mod/src/main/resources/assets/lovisual/textures/trollface/trollface_3.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_4.png b/mod/src/main/resources/assets/lovisual/textures/trollface/trollface_4.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_4.png rename to mod/src/main/resources/assets/lovisual/textures/trollface/trollface_4.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_5.png b/mod/src/main/resources/assets/lovisual/textures/trollface/trollface_5.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_5.png rename to mod/src/main/resources/assets/lovisual/textures/trollface/trollface_5.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_6.png b/mod/src/main/resources/assets/lovisual/textures/trollface/trollface_6.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_6.png rename to mod/src/main/resources/assets/lovisual/textures/trollface/trollface_6.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_classic.png b/mod/src/main/resources/assets/lovisual/textures/trollface/trollface_classic.png similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_classic.png rename to mod/src/main/resources/assets/lovisual/textures/trollface/trollface_classic.png diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/ui/api/actions.d.ts b/mod/src/main/resources/assets/lovisual/ui/api/actions.d.ts similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/ui/api/actions.d.ts rename to mod/src/main/resources/assets/lovisual/ui/api/actions.d.ts diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/ui/api/assets.d.ts b/mod/src/main/resources/assets/lovisual/ui/api/assets.d.ts similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/ui/api/assets.d.ts rename to mod/src/main/resources/assets/lovisual/ui/api/assets.d.ts diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/ui/api/clickgui/settings_panel.js b/mod/src/main/resources/assets/lovisual/ui/api/clickgui/settings_panel.js similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/ui/api/clickgui/settings_panel.js rename to mod/src/main/resources/assets/lovisual/ui/api/clickgui/settings_panel.js diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/ui/api/clickgui/theme_preview.js b/mod/src/main/resources/assets/lovisual/ui/api/clickgui/theme_preview.js similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/ui/api/clickgui/theme_preview.js rename to mod/src/main/resources/assets/lovisual/ui/api/clickgui/theme_preview.js diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/ui/api/hud/draggable/compact_stat.js b/mod/src/main/resources/assets/lovisual/ui/api/hud/draggable/compact_stat.js similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/ui/api/hud/draggable/compact_stat.js rename to mod/src/main/resources/assets/lovisual/ui/api/hud/draggable/compact_stat.js diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/ui/api/hud/draggable/inventory_panel.js b/mod/src/main/resources/assets/lovisual/ui/api/hud/draggable/inventory_panel.js similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/ui/api/hud/draggable/inventory_panel.js rename to mod/src/main/resources/assets/lovisual/ui/api/hud/draggable/inventory_panel.js diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/ui/api/hud/draggable/itemizer.js b/mod/src/main/resources/assets/lovisual/ui/api/hud/draggable/itemizer.js similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/ui/api/hud/draggable/itemizer.js rename to mod/src/main/resources/assets/lovisual/ui/api/hud/draggable/itemizer.js diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/ui/api/hud/draggable/list_panel.js b/mod/src/main/resources/assets/lovisual/ui/api/hud/draggable/list_panel.js similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/ui/api/hud/draggable/list_panel.js rename to mod/src/main/resources/assets/lovisual/ui/api/hud/draggable/list_panel.js diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/ui/api/hud/draggable/panel_base.js b/mod/src/main/resources/assets/lovisual/ui/api/hud/draggable/panel_base.js similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/ui/api/hud/draggable/panel_base.js rename to mod/src/main/resources/assets/lovisual/ui/api/hud/draggable/panel_base.js diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/ui/api/hud/draggable/triangulator.js b/mod/src/main/resources/assets/lovisual/ui/api/hud/draggable/triangulator.js similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/ui/api/hud/draggable/triangulator.js rename to mod/src/main/resources/assets/lovisual/ui/api/hud/draggable/triangulator.js diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/ui/api/hud/static/custom_health_bar.js b/mod/src/main/resources/assets/lovisual/ui/api/hud/static/custom_health_bar.js similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/ui/api/hud/static/custom_health_bar.js rename to mod/src/main/resources/assets/lovisual/ui/api/hud/static/custom_health_bar.js diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/ui/api/hud/static/dynamic_island.js b/mod/src/main/resources/assets/lovisual/ui/api/hud/static/dynamic_island.js similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/ui/api/hud/static/dynamic_island.js rename to mod/src/main/resources/assets/lovisual/ui/api/hud/static/dynamic_island.js diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/ui/api/hud/static/tooltip_panel.js b/mod/src/main/resources/assets/lovisual/ui/api/hud/static/tooltip_panel.js similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/ui/api/hud/static/tooltip_panel.js rename to mod/src/main/resources/assets/lovisual/ui/api/hud/static/tooltip_panel.js diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/ui/api/runtime.d.ts b/mod/src/main/resources/assets/lovisual/ui/api/runtime.d.ts similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/ui/api/runtime.d.ts rename to mod/src/main/resources/assets/lovisual/ui/api/runtime.d.ts diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/ui/api/theme.d.ts b/mod/src/main/resources/assets/lovisual/ui/api/theme.d.ts similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/ui/api/theme.d.ts rename to mod/src/main/resources/assets/lovisual/ui/api/theme.d.ts diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/ui/api/ui.d.ts b/mod/src/main/resources/assets/lovisual/ui/api/ui.d.ts similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/ui/api/ui.d.ts rename to mod/src/main/resources/assets/lovisual/ui/api/ui.d.ts diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/ui/api/ui.js b/mod/src/main/resources/assets/lovisual/ui/api/ui.js similarity index 100% rename from combatant-client-26.2/src/main/resources/assets/lovisual/ui/api/ui.js rename to mod/src/main/resources/assets/lovisual/ui/api/ui.js diff --git a/combatant-client-26.2/src/main/resources/fabric.mod.json b/mod/src/main/resources/fabric.mod.json similarity index 100% rename from combatant-client-26.2/src/main/resources/fabric.mod.json rename to mod/src/main/resources/fabric.mod.json diff --git a/combatant-client-26.2/src/main/resources/javet/natives/README.txt b/mod/src/main/resources/javet/natives/README.txt similarity index 100% rename from combatant-client-26.2/src/main/resources/javet/natives/README.txt rename to mod/src/main/resources/javet/natives/README.txt diff --git a/combatant-client-26.2/src/main/resources/lovisual.mixins.json b/mod/src/main/resources/lovisual.mixins.json similarity index 100% rename from combatant-client-26.2/src/main/resources/lovisual.mixins.json rename to mod/src/main/resources/lovisual.mixins.json diff --git a/combatant-client-26.2/src/main/resources/mediaplayerinfo/natives/win/MediaPlayerInfo.dll b/mod/src/main/resources/mediaplayerinfo/natives/win/MediaPlayerInfo.dll similarity index 100% rename from combatant-client-26.2/src/main/resources/mediaplayerinfo/natives/win/MediaPlayerInfo.dll rename to mod/src/main/resources/mediaplayerinfo/natives/win/MediaPlayerInfo.dll diff --git a/combatant-client-26.2/src/svgMsdfTool/java/dev/loki/lovisual/render/engine/svg/SvgMsdfPreprocessor.java b/mod/src/svgMsdfTool/java/dev/loki/lovisual/render/engine/svg/SvgMsdfPreprocessor.java similarity index 100% rename from combatant-client-26.2/src/svgMsdfTool/java/dev/loki/lovisual/render/engine/svg/SvgMsdfPreprocessor.java rename to mod/src/svgMsdfTool/java/dev/loki/lovisual/render/engine/svg/SvgMsdfPreprocessor.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/config/profile/ConfigProfileDiffEngineTest.java b/mod/src/test/java/dev/loki/lovisual/config/profile/ConfigProfileDiffEngineTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/config/profile/ConfigProfileDiffEngineTest.java rename to mod/src/test/java/dev/loki/lovisual/config/profile/ConfigProfileDiffEngineTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/config/profile/ConfigProfileStorageTest.java b/mod/src/test/java/dev/loki/lovisual/config/profile/ConfigProfileStorageTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/config/profile/ConfigProfileStorageTest.java rename to mod/src/test/java/dev/loki/lovisual/config/profile/ConfigProfileStorageTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/config/profile/ConfigProfileValueCodecTest.java b/mod/src/test/java/dev/loki/lovisual/config/profile/ConfigProfileValueCodecTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/config/profile/ConfigProfileValueCodecTest.java rename to mod/src/test/java/dev/loki/lovisual/config/profile/ConfigProfileValueCodecTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/config/values/ColorValueTest.java b/mod/src/test/java/dev/loki/lovisual/config/values/ColorValueTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/config/values/ColorValueTest.java rename to mod/src/test/java/dev/loki/lovisual/config/values/ColorValueTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/config/values/EnumValueTest.java b/mod/src/test/java/dev/loki/lovisual/config/values/EnumValueTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/config/values/EnumValueTest.java rename to mod/src/test/java/dev/loki/lovisual/config/values/EnumValueTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/config/values/ModeValueTest.java b/mod/src/test/java/dev/loki/lovisual/config/values/ModeValueTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/config/values/ModeValueTest.java rename to mod/src/test/java/dev/loki/lovisual/config/values/ModeValueTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/config/values/PrimitiveValuesTest.java b/mod/src/test/java/dev/loki/lovisual/config/values/PrimitiveValuesTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/config/values/PrimitiveValuesTest.java rename to mod/src/test/java/dev/loki/lovisual/config/values/PrimitiveValuesTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/events/EventBusTest.java b/mod/src/test/java/dev/loki/lovisual/events/EventBusTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/events/EventBusTest.java rename to mod/src/test/java/dev/loki/lovisual/events/EventBusTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/command/CommandDiscoveryTest.java b/mod/src/test/java/dev/loki/lovisual/features/command/CommandDiscoveryTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/features/command/CommandDiscoveryTest.java rename to mod/src/test/java/dev/loki/lovisual/features/command/CommandDiscoveryTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/command/CommandPrefixTest.java b/mod/src/test/java/dev/loki/lovisual/features/command/CommandPrefixTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/features/command/CommandPrefixTest.java rename to mod/src/test/java/dev/loki/lovisual/features/command/CommandPrefixTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/command/UsageHintsTest.java b/mod/src/test/java/dev/loki/lovisual/features/command/UsageHintsTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/features/command/UsageHintsTest.java rename to mod/src/test/java/dev/loki/lovisual/features/command/UsageHintsTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/command/impl/world/XaeroWaypointCommandTest.java b/mod/src/test/java/dev/loki/lovisual/features/command/impl/world/XaeroWaypointCommandTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/features/command/impl/world/XaeroWaypointCommandTest.java rename to mod/src/test/java/dev/loki/lovisual/features/command/impl/world/XaeroWaypointCommandTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/MenuSettingsResolverTest.java b/mod/src/test/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/MenuSettingsResolverTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/MenuSettingsResolverTest.java rename to mod/src/test/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/MenuSettingsResolverTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/gui/hud/HudElementAutoLoaderTest.java b/mod/src/test/java/dev/loki/lovisual/features/gui/hud/HudElementAutoLoaderTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/features/gui/hud/HudElementAutoLoaderTest.java rename to mod/src/test/java/dev/loki/lovisual/features/gui/hud/HudElementAutoLoaderTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/TriangulatorSolverTest.java b/mod/src/test/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/TriangulatorSolverTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/TriangulatorSolverTest.java rename to mod/src/test/java/dev/loki/lovisual/features/gui/hud/draggable/impl/triangulator/TriangulatorSolverTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/combat/combatlog/CombatLogStatsTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/combat/combatlog/CombatLogStatsTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/combat/combatlog/CombatLogStatsTest.java rename to mod/src/test/java/dev/loki/lovisual/features/module/modules/combat/combatlog/CombatLogStatsTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/misc/items/ScrollMovePolicyTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/items/ScrollMovePolicyTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/misc/items/ScrollMovePolicyTest.java rename to mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/items/ScrollMovePolicyTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/misc/protect/NoServerPackTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/protect/NoServerPackTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/misc/protect/NoServerPackTest.java rename to mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/protect/NoServerPackTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/misc/sound/NoSoundTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/sound/NoSoundTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/misc/sound/NoSoundTest.java rename to mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/sound/NoSoundTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/DeathCoordsTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/player/DeathCoordsTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/DeathCoordsTest.java rename to mod/src/test/java/dev/loki/lovisual/features/module/modules/player/DeathCoordsTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/UseTrackerTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/player/UseTrackerTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/UseTrackerTest.java rename to mod/src/test/java/dev/loki/lovisual/features/module/modules/player/UseTrackerTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/auto/AutoAcceptTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/player/auto/AutoAcceptTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/auto/AutoAcceptTest.java rename to mod/src/test/java/dev/loki/lovisual/features/module/modules/player/auto/AutoAcceptTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/auto/AutoLeaveTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/player/auto/AutoLeaveTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/auto/AutoLeaveTest.java rename to mod/src/test/java/dev/loki/lovisual/features/module/modules/player/auto/AutoLeaveTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/auto/AutoRespawnTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/player/auto/AutoRespawnTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/auto/AutoRespawnTest.java rename to mod/src/test/java/dev/loki/lovisual/features/module/modules/player/auto/AutoRespawnTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/interact/NoInteractTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/player/interact/NoInteractTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/interact/NoInteractTest.java rename to mod/src/test/java/dev/loki/lovisual/features/module/modules/player/interact/NoInteractTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/inventory/TapeMouseTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/player/inventory/TapeMouseTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/inventory/TapeMouseTest.java rename to mod/src/test/java/dev/loki/lovisual/features/module/modules/player/inventory/TapeMouseTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/AlwaysVisibleHandTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/AlwaysVisibleHandTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/AlwaysVisibleHandTest.java rename to mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/AlwaysVisibleHandTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/AtmoDawnFogTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/AtmoDawnFogTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/AtmoDawnFogTest.java rename to mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/AtmoDawnFogTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/ChinaHatModelTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/ChinaHatModelTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/ChinaHatModelTest.java rename to mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/ChinaHatModelTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/ChinaHatPaletteTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/ChinaHatPaletteTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/ChinaHatPaletteTest.java rename to mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/ChinaHatPaletteTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/LightTintTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/LightTintTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/LightTintTest.java rename to mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/LightTintTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/PostFxPresetTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/PostFxPresetTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/PostFxPresetTest.java rename to mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/PostFxPresetTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/LrcLibClientTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/LrcLibClientTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/LrcLibClientTest.java rename to mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/LrcLibClientTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/LrcParserTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/LrcParserTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/LrcParserTest.java rename to mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/LrcParserTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/LyricLineSplitterTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/LyricLineSplitterTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/LyricLineSplitterTest.java rename to mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/LyricLineSplitterTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/wings/WingShapesTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/wings/WingShapesTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/wings/WingShapesTest.java rename to mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/wings/WingShapesTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/security/BackdoorProtectionTest.java b/mod/src/test/java/dev/loki/lovisual/features/security/BackdoorProtectionTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/features/security/BackdoorProtectionTest.java rename to mod/src/test/java/dev/loki/lovisual/features/security/BackdoorProtectionTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/JavetUiScriptEngineTest.java b/mod/src/test/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/JavetUiScriptEngineTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/JavetUiScriptEngineTest.java rename to mod/src/test/java/dev/loki/lovisual/render/engine/renderer/ui/runtime/script/JavetUiScriptEngineTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/EaseCurvesTest.java b/mod/src/test/java/dev/loki/lovisual/util/animation/EaseCurvesTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/EaseCurvesTest.java rename to mod/src/test/java/dev/loki/lovisual/util/animation/EaseCurvesTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/EquipProgressComposerTest.java b/mod/src/test/java/dev/loki/lovisual/util/animation/EquipProgressComposerTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/EquipProgressComposerTest.java rename to mod/src/test/java/dev/loki/lovisual/util/animation/EquipProgressComposerTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/SmoothValueTest.java b/mod/src/test/java/dev/loki/lovisual/util/animation/SmoothValueTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/SmoothValueTest.java rename to mod/src/test/java/dev/loki/lovisual/util/animation/SmoothValueTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/ThreeStageProgressTest.java b/mod/src/test/java/dev/loki/lovisual/util/animation/ThreeStageProgressTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/ThreeStageProgressTest.java rename to mod/src/test/java/dev/loki/lovisual/util/animation/ThreeStageProgressTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/hand/BobMathTest.java b/mod/src/test/java/dev/loki/lovisual/util/animation/hand/BobMathTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/hand/BobMathTest.java rename to mod/src/test/java/dev/loki/lovisual/util/animation/hand/BobMathTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/hand/HandPoseMathTest.java b/mod/src/test/java/dev/loki/lovisual/util/animation/hand/HandPoseMathTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/hand/HandPoseMathTest.java rename to mod/src/test/java/dev/loki/lovisual/util/animation/hand/HandPoseMathTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/hand/state/HandAnimStateTest.java b/mod/src/test/java/dev/loki/lovisual/util/animation/hand/state/HandAnimStateTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/hand/state/HandAnimStateTest.java rename to mod/src/test/java/dev/loki/lovisual/util/animation/hand/state/HandAnimStateTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/hand/state/SwayStateTest.java b/mod/src/test/java/dev/loki/lovisual/util/animation/hand/state/SwayStateTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/hand/state/SwayStateTest.java rename to mod/src/test/java/dev/loki/lovisual/util/animation/hand/state/SwayStateTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/combat/ReachMeterTest.java b/mod/src/test/java/dev/loki/lovisual/util/combat/ReachMeterTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/util/combat/ReachMeterTest.java rename to mod/src/test/java/dev/loki/lovisual/util/combat/ReachMeterTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/mctiers/MCTiersTierTest.java b/mod/src/test/java/dev/loki/lovisual/util/mctiers/MCTiersTierTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/util/mctiers/MCTiersTierTest.java rename to mod/src/test/java/dev/loki/lovisual/util/mctiers/MCTiersTierTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/physics/TrinketPhysicsTest.java b/mod/src/test/java/dev/loki/lovisual/util/physics/TrinketPhysicsTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/util/physics/TrinketPhysicsTest.java rename to mod/src/test/java/dev/loki/lovisual/util/physics/TrinketPhysicsTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/protection/NotifyThrottleTest.java b/mod/src/test/java/dev/loki/lovisual/util/protection/NotifyThrottleTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/util/protection/NotifyThrottleTest.java rename to mod/src/test/java/dev/loki/lovisual/util/protection/NotifyThrottleTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/protection/SlotLockPolicyTest.java b/mod/src/test/java/dev/loki/lovisual/util/protection/SlotLockPolicyTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/util/protection/SlotLockPolicyTest.java rename to mod/src/test/java/dev/loki/lovisual/util/protection/SlotLockPolicyTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/text/LegacyTextUtilTest.java b/mod/src/test/java/dev/loki/lovisual/util/text/LegacyTextUtilTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/util/text/LegacyTextUtilTest.java rename to mod/src/test/java/dev/loki/lovisual/util/text/LegacyTextUtilTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/text/NameMaskerTest.java b/mod/src/test/java/dev/loki/lovisual/util/text/NameMaskerTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/util/text/NameMaskerTest.java rename to mod/src/test/java/dev/loki/lovisual/util/text/NameMaskerTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/text/NameProtectorTest.java b/mod/src/test/java/dev/loki/lovisual/util/text/NameProtectorTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/util/text/NameProtectorTest.java rename to mod/src/test/java/dev/loki/lovisual/util/text/NameProtectorTest.java diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/world/GpsBearingTest.java b/mod/src/test/java/dev/loki/lovisual/util/world/GpsBearingTest.java similarity index 100% rename from combatant-client-26.2/src/test/java/dev/loki/lovisual/util/world/GpsBearingTest.java rename to mod/src/test/java/dev/loki/lovisual/util/world/GpsBearingTest.java diff --git a/combatant-client-26.2/src/vulkanDebug/java/dev/loki/lovisual/mixins/CommandEncoderDiagnosticsMixin.java b/mod/src/vulkanDebug/java/dev/loki/lovisual/mixins/CommandEncoderDiagnosticsMixin.java similarity index 100% rename from combatant-client-26.2/src/vulkanDebug/java/dev/loki/lovisual/mixins/CommandEncoderDiagnosticsMixin.java rename to mod/src/vulkanDebug/java/dev/loki/lovisual/mixins/CommandEncoderDiagnosticsMixin.java diff --git a/combatant-client-26.2/src/vulkanDebug/java/dev/loki/lovisual/mixins/RenderPassWrapperDiagnosticsMixin.java b/mod/src/vulkanDebug/java/dev/loki/lovisual/mixins/RenderPassWrapperDiagnosticsMixin.java similarity index 100% rename from combatant-client-26.2/src/vulkanDebug/java/dev/loki/lovisual/mixins/RenderPassWrapperDiagnosticsMixin.java rename to mod/src/vulkanDebug/java/dev/loki/lovisual/mixins/RenderPassWrapperDiagnosticsMixin.java diff --git a/combatant-client-26.2/src/vulkanDebug/java/dev/loki/lovisual/mixins/VulkanBackendMixin.java b/mod/src/vulkanDebug/java/dev/loki/lovisual/mixins/VulkanBackendMixin.java similarity index 100% rename from combatant-client-26.2/src/vulkanDebug/java/dev/loki/lovisual/mixins/VulkanBackendMixin.java rename to mod/src/vulkanDebug/java/dev/loki/lovisual/mixins/VulkanBackendMixin.java diff --git a/combatant-client-26.2/src/vulkanDebug/java/dev/loki/lovisual/mixins/VulkanCheckpointDiagnosticsMixin.java b/mod/src/vulkanDebug/java/dev/loki/lovisual/mixins/VulkanCheckpointDiagnosticsMixin.java similarity index 100% rename from combatant-client-26.2/src/vulkanDebug/java/dev/loki/lovisual/mixins/VulkanCheckpointDiagnosticsMixin.java rename to mod/src/vulkanDebug/java/dev/loki/lovisual/mixins/VulkanCheckpointDiagnosticsMixin.java diff --git a/combatant-client-26.2/src/vulkanDebug/java/dev/loki/lovisual/mixins/VulkanCommandEncoderDiagnosticsMixin.java b/mod/src/vulkanDebug/java/dev/loki/lovisual/mixins/VulkanCommandEncoderDiagnosticsMixin.java similarity index 100% rename from combatant-client-26.2/src/vulkanDebug/java/dev/loki/lovisual/mixins/VulkanCommandEncoderDiagnosticsMixin.java rename to mod/src/vulkanDebug/java/dev/loki/lovisual/mixins/VulkanCommandEncoderDiagnosticsMixin.java diff --git a/combatant-client-26.2/src/vulkanDebug/java/dev/loki/lovisual/mixins/VulkanDebugMessageMixin.java b/mod/src/vulkanDebug/java/dev/loki/lovisual/mixins/VulkanDebugMessageMixin.java similarity index 100% rename from combatant-client-26.2/src/vulkanDebug/java/dev/loki/lovisual/mixins/VulkanDebugMessageMixin.java rename to mod/src/vulkanDebug/java/dev/loki/lovisual/mixins/VulkanDebugMessageMixin.java diff --git a/combatant-client-26.2/src/vulkanDebug/java/dev/loki/lovisual/mixins/VulkanDeviceDiagnosticsMixin.java b/mod/src/vulkanDebug/java/dev/loki/lovisual/mixins/VulkanDeviceDiagnosticsMixin.java similarity index 100% rename from combatant-client-26.2/src/vulkanDebug/java/dev/loki/lovisual/mixins/VulkanDeviceDiagnosticsMixin.java rename to mod/src/vulkanDebug/java/dev/loki/lovisual/mixins/VulkanDeviceDiagnosticsMixin.java diff --git a/combatant-client-26.2/src/vulkanDebug/java/dev/loki/lovisual/mixins/VulkanQueueSubmissionMixin.java b/mod/src/vulkanDebug/java/dev/loki/lovisual/mixins/VulkanQueueSubmissionMixin.java similarity index 100% rename from combatant-client-26.2/src/vulkanDebug/java/dev/loki/lovisual/mixins/VulkanQueueSubmissionMixin.java rename to mod/src/vulkanDebug/java/dev/loki/lovisual/mixins/VulkanQueueSubmissionMixin.java diff --git a/combatant-client-26.2/src/vulkanDebug/java/dev/loki/lovisual/mixins/VulkanRenderPassDiagnosticsMixin.java b/mod/src/vulkanDebug/java/dev/loki/lovisual/mixins/VulkanRenderPassDiagnosticsMixin.java similarity index 100% rename from combatant-client-26.2/src/vulkanDebug/java/dev/loki/lovisual/mixins/VulkanRenderPassDiagnosticsMixin.java rename to mod/src/vulkanDebug/java/dev/loki/lovisual/mixins/VulkanRenderPassDiagnosticsMixin.java diff --git a/combatant-client-26.2/src/vulkanDebug/java/dev/loki/lovisual/mixins/VulkanRenderPipelineDiagnosticsMixin.java b/mod/src/vulkanDebug/java/dev/loki/lovisual/mixins/VulkanRenderPipelineDiagnosticsMixin.java similarity index 100% rename from combatant-client-26.2/src/vulkanDebug/java/dev/loki/lovisual/mixins/VulkanRenderPipelineDiagnosticsMixin.java rename to mod/src/vulkanDebug/java/dev/loki/lovisual/mixins/VulkanRenderPipelineDiagnosticsMixin.java diff --git a/combatant-client-26.2/src/vulkanDebug/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/debug/VulkanCrashDiagnostics.java b/mod/src/vulkanDebug/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/debug/VulkanCrashDiagnostics.java similarity index 100% rename from combatant-client-26.2/src/vulkanDebug/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/debug/VulkanCrashDiagnostics.java rename to mod/src/vulkanDebug/java/dev/loki/lovisual/render/engine/rhi/backend/vulkan/debug/VulkanCrashDiagnostics.java diff --git a/combatant-client-26.2/src/vulkanDebug/resources/lovisual-vulkan-debug.mixins.json b/mod/src/vulkanDebug/resources/lovisual-vulkan-debug.mixins.json similarity index 100% rename from combatant-client-26.2/src/vulkanDebug/resources/lovisual-vulkan-debug.mixins.json rename to mod/src/vulkanDebug/resources/lovisual-vulkan-debug.mixins.json diff --git a/combatant-client-26.2/tools/msdf/GenerateSvgMsdf.ps1 b/mod/tools/msdf/GenerateSvgMsdf.ps1 similarity index 100% rename from combatant-client-26.2/tools/msdf/GenerateSvgMsdf.ps1 rename to mod/tools/msdf/GenerateSvgMsdf.ps1 diff --git a/combatant-client-26.2/tools/msdf/msdf-atlas-gen.exe b/mod/tools/msdf/msdf-atlas-gen.exe similarity index 100% rename from combatant-client-26.2/tools/msdf/msdf-atlas-gen.exe rename to mod/tools/msdf/msdf-atlas-gen.exe diff --git a/combatant-client-26.2/tools/msdf/msdf-atlas-gen/msdf-atlas-gen.exe b/mod/tools/msdf/msdf-atlas-gen/msdf-atlas-gen.exe similarity index 100% rename from combatant-client-26.2/tools/msdf/msdf-atlas-gen/msdf-atlas-gen.exe rename to mod/tools/msdf/msdf-atlas-gen/msdf-atlas-gen.exe diff --git a/combatant-client-26.2/tools/msdf/msdfgen-1.13-win64/msdfgen/example.bat b/mod/tools/msdf/msdfgen-1.13-win64/msdfgen/example.bat similarity index 100% rename from combatant-client-26.2/tools/msdf/msdfgen-1.13-win64/msdfgen/example.bat rename to mod/tools/msdf/msdfgen-1.13-win64/msdfgen/example.bat diff --git a/combatant-client-26.2/tools/msdf/msdfgen-1.13-win64/msdfgen/msdfgen.exe b/mod/tools/msdf/msdfgen-1.13-win64/msdfgen/msdfgen.exe similarity index 100% rename from combatant-client-26.2/tools/msdf/msdfgen-1.13-win64/msdfgen/msdfgen.exe rename to mod/tools/msdf/msdfgen-1.13-win64/msdfgen/msdfgen.exe diff --git a/combatant-client-26.2/tools/msdf/msdfgen.exe b/mod/tools/msdf/msdfgen.exe similarity index 100% rename from combatant-client-26.2/tools/msdf/msdfgen.exe rename to mod/tools/msdf/msdfgen.exe diff --git a/combatant-client-26.2/tools/pkgsplit/fiximports.py b/mod/tools/pkgsplit/fiximports.py similarity index 100% rename from combatant-client-26.2/tools/pkgsplit/fiximports.py rename to mod/tools/pkgsplit/fiximports.py diff --git a/combatant-client-26.2/tools/pkgsplit/member_sweep.py b/mod/tools/pkgsplit/member_sweep.py similarity index 100% rename from combatant-client-26.2/tools/pkgsplit/member_sweep.py rename to mod/tools/pkgsplit/member_sweep.py diff --git a/combatant-client-26.2/tools/pkgsplit/promote.py b/mod/tools/pkgsplit/promote.py similarity index 100% rename from combatant-client-26.2/tools/pkgsplit/promote.py rename to mod/tools/pkgsplit/promote.py diff --git a/combatant-client-26.2/tools/pkgsplit/split_struct.py b/mod/tools/pkgsplit/split_struct.py similarity index 100% rename from combatant-client-26.2/tools/pkgsplit/split_struct.py rename to mod/tools/pkgsplit/split_struct.py