chore(history): squash 15 commit(s) from 2026-09-22
- refactor(mixins): разбивка render/entity (crystal/item) и render/level/particles — регресс 8.5.1 - refactor(structure): разбивка 10 папок >4 файлов — регресс 8.5.1 после Фаз 7/10 - refactor(ui): OrderedUiBatcher 944 → 188 + 8 хелперов ≤200 (8.5.2, гейт 9.2) - refactor(text): MsdfFont 613 → 163 + atlas/ (Atlas,AtlasLoader,Json,Glyph) + GlyphDrawer 179 (8.5.2, гейт 9.2) - refactor(text): CustomTextRenderer 540 → 235 + CustomFontSet 142 + GradientTexts 95 + HorizontalFade 77 (8.5.2, гейт 9.2) - refactor(buttons): BetterButtons 1033 → 388 + 7 хелперов ≤200 (8.5.2, гейт 9.2) - refactor(inventory): удалить InventorySwap 1038 + ShitDropper → ShitESP (Вариант A) - fix(shitesp): цвет через color() helper как у ESP/Chams (крутой пиккер) - fix(blockesp): камень→спавнер подсвечивается сразу, docs: DEV_GUIDE + DEVELOPMENT → один гайд - docs(dev-guide): дополнен §18 современными правилами (web-поиск 2025-2026) - chore(quality): PMD 7.27.0 + Checkstyle 14.1.0 + SpotBugs 6.5.11 + обновы зависимостей - refactor(svg): SvgMeshBackend 1020 → 611 + SvgParser 181 + SvgGeometry 173 (8.5.2) - refactor(svg): SvgMeshBackend 611 → 267 + SvgTexture 223 + SvgRender 35 + SvgCache 25 (8.5.2) - refactor(theme): Themes 1012 → 562 + 3×163 пресета (8.5.2) - refactor(particles): WorldParticles 999 → 821 + geometry 93 + sphere 67 (8.5.2)
This commit is contained in:
parent
8181f11c5a
commit
ec10395511
140 changed files with 5442 additions and 6457 deletions
|
|
@ -1,200 +0,0 @@
|
|||
# LoVisual — руководство разработчика
|
||||
|
||||
Как устроен проект, как добавлять фичи и какие правила обязательны.
|
||||
Версии: **Java 25 · Fabric Loader 0.19.3 · Minecraft 26.2** (Loom).
|
||||
|
||||
---
|
||||
|
||||
## 1. Сборка и запуск
|
||||
|
||||
```bash
|
||||
cd combatant-client-26.2
|
||||
./gradlew build # jar: build/libs/lovisual-<ver>.jar
|
||||
./gradlew test # юнит-тесты (сейчас ~320)
|
||||
./gradlew runClient # быстрый dev-запуск Minecraft с модом
|
||||
./gradlew runClient --rerun-tasks # если кэш врёт
|
||||
```
|
||||
|
||||
Готовый jar для теста в реальной сборке — скопировать в инстанс:
|
||||
|
||||
```bash
|
||||
cp build/libs/lovisual-*.jar /storage/prismlauncher/instances/26.2/minecraft/mods/
|
||||
```
|
||||
|
||||
Ресурсы (текстуры) — **только настоящие PNG** (сигнатура `89 50 4E 47`).
|
||||
JPEG, переименованный в .png, уронит `Bad PNG Signature` при загрузке ресурспака.
|
||||
|
||||
---
|
||||
|
||||
## 2. Правила проекта (нарушение = откат)
|
||||
|
||||
- **KISS / DRY / SOLID** — простая структура, без оверинжиниринга.
|
||||
- **≤ 200 строк на файл.** Фасад-делегат (например `Renderer2D`) — исключение.
|
||||
Больше логики — выноси package-private хелперы в тот же пакет.
|
||||
- **≤ 4 файла на папку** — больше, дели папку по смыслу (подпапка).
|
||||
- **Никакого статики от Minecraft**: `Minecraft.getInstance()` — только локальной
|
||||
переменной в месте использования, НИКОГДА в поле (`private final Minecraft mc = ...` — баг).
|
||||
Функциональный синглтон (`INSTANCE`) — только для сервисов без состояния жизненного цикла.
|
||||
- **`Lifecycle.isActive()/canRunHud()` — гейты** для рендера/событий; `EventBus.post()`
|
||||
сам игнорирует события до activation.
|
||||
- **Никаких this-escape** — конструктор не публикует `this`.
|
||||
- **Никаких мёртвых миксинов** — миксин только если реально нужен;
|
||||
`cancellable=true` только при реальном cancel.
|
||||
- **`dev.loki.lovisual.mixins.*` — ТОЛЬКО классы с `@Mixin`.** Обычный helper-класс
|
||||
внутри mixin-пакета крашит игру: Knot не даёт transformed-классу обратиться к нему
|
||||
(`IllegalClassLoadError`). Хелперы — в `util/` или пакет фичи (см. `HandDynamics`).
|
||||
- **Без пустышек** — файл без реализации не создаётся.
|
||||
- **Каждый этап компилируется**: `./gradlew build` перед коммитом.
|
||||
- **Тесты обязательны** для новой чистой логики (JUnit 5, без моков Minecraft —
|
||||
выноси математику/парсинг в чистые статические методы и тесть их).
|
||||
- **Референсы (`ref/`)** — чужой код (SoupVisuals SOUP-1.0, Delta и др.):
|
||||
**только идеи**, пере-реализация под наши правила, никакого копирования.
|
||||
|
||||
---
|
||||
|
||||
## 3. Карта пакетов
|
||||
|
||||
```
|
||||
dev.loki.lovisual
|
||||
├── LoVisual.java — инициализация ClientModInitializer
|
||||
├── Lifecycle.java — INIT → ACTIVE → SHUTDOWN, гейты canRunHud/Render
|
||||
├── events/ — EventBus (@EventHandler, приоритеты, гейт по Module)
|
||||
│ └── impl/ — конкретные события (Tick, RenderHud, Packet, Lightmap...)
|
||||
├── config/ — MainConfig, SettingDef, сериализация (.lvcfg, legacy .cbcfg)
|
||||
│ ├── values/ — ConfigValue: bool/num/mode/color/set/... (есть тесты)
|
||||
│ └── profile/ — профили конфигов (collect/apply/diff/binary codec)
|
||||
├── features/
|
||||
│ ├── module/ — Module, ModuleCategory, ModuleInfo, ModuleManager,
|
||||
│ │ └── modules/... — сами модули по категориям
|
||||
│ ├── gui/ — ClickGUI, HUD (draggable + nondraggable), BetterChat
|
||||
│ ├── command/ — чат-команды (@CommandInfo, ClassGraph-поиск)
|
||||
│ ├── relations/ — друзья/враги/стаф (PlayerRelations, CategoryService)
|
||||
│ └── security/ — AntiBot-фильтр, StaffTracker
|
||||
├── render/engine/ — Renderer2D/3D, пайплайны, шрифты, MeshBuilder
|
||||
├── mixins/ — @Mixin-классы (см. правило выше)
|
||||
└── util/ — чистые утилиты (text/animation/time/...) — тут живут тесты
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Как добавить МОДУЛЬ
|
||||
|
||||
Модули находятся в `features/module/modules/<категория>/`. Категории:
|
||||
`COMBAT · MOVEMENT · PLAYER · VISUALS · MISC` (см. `ModuleCategory`).
|
||||
Легит-направление: читерские модули (автоудары, полёты, speed...) **удаляем, не добавляем**.
|
||||
|
||||
Регистрация автоматическая: **ClassGraph сканирует `@ModuleInfo`** — ничего никуда
|
||||
дописывать не надо. Пустой конструктор обязателен.
|
||||
|
||||
Шаблон:
|
||||
|
||||
```java
|
||||
@ModuleInfo(id = "mymodule", displayName = "My Module", category = ModuleCategory.VISUALS)
|
||||
public class MyModule extends Module {
|
||||
|
||||
private static final String SETTING_X = "x_amount";
|
||||
private final NumberValue<Integer> amount = num("myModuleAmount", SETTING_X, 5, 1, 10);
|
||||
|
||||
@Override
|
||||
public void onTick() {
|
||||
Minecraft mc = Minecraft.getInstance(); // локально, не в поле!
|
||||
if (mc.level == null) return;
|
||||
// ...
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
private void onRenderHud(RenderHudEvent event) { /* авто-подписка через EventBus */ }
|
||||
}
|
||||
```
|
||||
|
||||
Ключевые хуки `Module`: `onEnable/onDisable/onTick/onFrame`,
|
||||
`onRenderHudEngine(...)`, `onRenderWorldEngine(...)`, `getWorldPhase()/getHudPhase()`.
|
||||
Хелперы настроек сами объявляют `SettingDef`: `bool/num/mode/color/colorNoAlpha/
|
||||
enumSetting/text/group/bind` + `visibleWhen(value, () -> cond)`.
|
||||
Модуль-лист по умолчанию недоступен цели — смотри также `boolCommon`/`modeCommon`
|
||||
для общих схем (`CommonSettingSchemas`).
|
||||
|
||||
Доступ к чужому модулю: `Modules.get(X.class)`, проверка состояния — `isEnabled()`.
|
||||
|
||||
Мировой рендер (боксы/линии) — через `Renderer3D.batch(WORLD_COLORED_LINES, DepthMode...)`
|
||||
и MeshBuilder; готовые куски геометрии — `EspMeshBuilder.addOutlineBox` и т.п.
|
||||
|
||||
---
|
||||
|
||||
## 5. Как добавить HUD-элемент
|
||||
|
||||
Два вида, оба ищутся ClassGraph-ом автоматически:
|
||||
|
||||
**A. Draggable (перетаскиваемый, вкладка Settings → HUD):**
|
||||
`features/gui/hud/draggable/impl/`, аннотация `@HudElementInfo(id, displayName,
|
||||
enabledByDefault, order)`, базовый класс `DraggableHudElement`.
|
||||
Обязательно: `applyDefaultPosition(screenW, screenH)`, рендер в `renderEngine(...)`,
|
||||
`usesEngineRenderer() = true`. Позиция сохраняется через `layoutValue`.
|
||||
|
||||
**B. Static (фиксированный, вкладка Settings → UI):**
|
||||
`features/gui/hud/nondraggable/impl/`, аннотация `@HudElementRegister(order=N)`,
|
||||
базовый класс `AbstractHudElement`, конструктор `super(id, "Title", defaultEnabled)`,
|
||||
синглтон `public static final X INSTANCE` (лоадер забирает его из статического поля).
|
||||
Чтобы карточка появилась во вкладке **UI**, добавь `new StaticHudCard("<id>", "<Title>")`
|
||||
в `STATIC_UI_CARDS` в `MenuSettingsResolver.java` (draggable-виджеты там не нужны —
|
||||
они собираются из реестра сами).
|
||||
|
||||
Цвет из темы: `Theme.theme().accent()/windowBg()/textMuted()`; альфа —
|
||||
`HudRenderUtil.setAlpha(argb, a)`; текст — `Fonts.renderer(name, type, fallback)`
|
||||
(`begin(scale,...)` → `getWidth`/`render` → `end()`).
|
||||
|
||||
---
|
||||
|
||||
## 6. Как добавить СОБЫТИЕ / МИКСИН
|
||||
|
||||
1. Событие: наследуй `Event` (есть `cancel()`), положи в `events/impl/`.
|
||||
2. Публикация: `Events.BUS.post(new MyEvent(...))` из миксина.
|
||||
3. Миксин: класс с `@Mixin(Target.class)` в `mixins/` **и имя в `lovisual.mixins.json`**
|
||||
(секции `mixins`/`client`). Миксин, которого нет в json, не применится.
|
||||
4. Если миксин опциональный (зависит от чужого мода, напр. Sodium) — цель может
|
||||
отсутствовать: Mixin напечатает WARN и пройдёт (см. `SodiumChunkMeshFormatsMixin`).
|
||||
Такой WARN — норма, краш — нет.
|
||||
5. Чистая логика из миксина — вобы в обычный класс вне `mixins/`-пакета (см. правило про `HandDynamics`).
|
||||
|
||||
---
|
||||
|
||||
## 7. Настройки, конфиг и i18n
|
||||
|
||||
- Конфиги модулей/HUD пишутся в `<game>/lovisual/` в формате **`.lvcfg`**
|
||||
(legacy `.cbcfg` читается). Профили — `config/profile/` (кнопка в ClickGUI → Configs).
|
||||
- Каждой настройке нужен i18n-ключ в **обоих** файлах
|
||||
`src/main/resources/assets/lovisual/lang/{en_us,ru_ru}.json`:
|
||||
- модуль: `module.<id>.description`, настройки `setting.<id>.<setting_id>`,
|
||||
опции режима `setting.<id>.<setting_id>.option.<Option>`;
|
||||
- draggable HUD: `hud.draggable.<id>` / `setting.draggable.<id>.<key>`;
|
||||
- static HUD: `setting.hud_element.<id>.<key>` (см. Watermark, Swap Tooltip).
|
||||
- `setting_id` — snake_case (`"show_ping"`); configName —camelCase с префиксом мода
|
||||
(`"watermark_show_ping"`) — из него авто-выводится id.
|
||||
- Пропущенный ключ ловит `MissingI18nReporter` в дев-окружении — проверяй консоль.
|
||||
|
||||
---
|
||||
|
||||
## 8. Тесты
|
||||
|
||||
`src/test/java/` зеркалит main. Пишутся **только на чистую логику** (без Minecraft-
|
||||
рантайма): парсеры, математика, codec'ы, матчинги. Примеры:
|
||||
- `util/text/NameProtectorTest`, `LegacyTextUtilTest` — парсинг/подмена,
|
||||
- `config/values/*Test` — значения конфига,
|
||||
- `config/profile/ConfigProfileValueCodecTest` — бинарный round-trip,
|
||||
- `events/EventBusTest` — события (нужен `Lifecycle.activate()` в `@BeforeAll`).
|
||||
|
||||
Если фича завязана на Minecraft — выделяй статический pure-метод
|
||||
(`static int tint(...)` / `static record Layout(...)`) и тесть его (см. `Light.tintAmbient`).
|
||||
|
||||
---
|
||||
|
||||
## 9. Чеклист перед коммитом
|
||||
|
||||
- [ ] `./gradlew build` зелёный, `./gradlew test` зелёный
|
||||
- [ ] новых файлов >200 строк нет; папка не разрослась >4 файлов
|
||||
- [ ] нет полей `Minecraft` / `getInstance()` в полях
|
||||
- [ ] lang-ключи en_us + ru_ru добавлены для всех новых настроек
|
||||
- [ ] тест на новую чистую логику есть
|
||||
- [ ] в mixin-пакете только `@Mixin`-классы
|
||||
- [ ] TODO.md обновлён (галка/заметка), если это этап плана
|
||||
- [ ] в чужой код (`ref/`) не заглядывался копипаст
|
||||
|
|
@ -1,569 +1,297 @@
|
|||
# LoVisual Mod — Dev Guide / Справочник по созданию модулей
|
||||
# LoVisual Mod — Dev Guide (Единый справочник)
|
||||
|
||||
Цель: быстро посмотреть структуру и написать новый модуль / HUD-элемент /
|
||||
команду, не перекапывая весь код. Всё ниже — сводка по реальному коду.
|
||||
Цель: быстро добавить модуль / HUD-элемент / команду / миксин, не перекапывая весь код. Сводка по реальному коду, объединено из `DEV_GUIDE.md` + `DEVELOPMENT.md`.
|
||||
|
||||
- Пакеты: `dev.loki.lovisual.*`
|
||||
- Minecraft 26.2, Fabric, Java 25, Lombok
|
||||
- Правила проекта (важно!): файл ≤200 строк, папка ≤3 файлов,
|
||||
`./gradlew build` после каждого этапа, тесты для новой логики,
|
||||
никаких `static final Minecraft` в полях — только ленивый `Minecraft.getInstance()`.
|
||||
- Стек: **Java 25 · Fabric Loader 0.19.3 · Minecraft 26.2** (Fabric Loom), Lombok
|
||||
- Правила — нарушение = откат (см. §2)
|
||||
|
||||
---
|
||||
|
||||
## 1. Архитектура (главные пакеты)
|
||||
## 1. Сборка и запуск
|
||||
|
||||
```bash
|
||||
cd combatant-client-26.2
|
||||
./gradlew build # jar: build/libs/lovisual-<ver>.jar
|
||||
./gradlew test # юнит-тесты (~320)
|
||||
./gradlew runClient # dev-запуск Minecraft с модом
|
||||
./gradlew runClient --rerun-tasks # если кэш врёт
|
||||
```
|
||||
|
||||
Готовый jar для теста в реальном инстансе:
|
||||
|
||||
```bash
|
||||
cp build/libs/lovisual-*.jar /storage/prismlauncher/instances/26.2/minecraft/mods/
|
||||
```
|
||||
|
||||
Ресурсы: **только настоящие PNG** (сигнатура `89 50 4E 47`). JPEG, переименованный в `.png`, уронит `Bad PNG Signature` при загрузке.
|
||||
|
||||
---
|
||||
|
||||
## 2. Правила проекта (ОБЯЗАТЕЛЬНЫЕ)
|
||||
|
||||
- **KISS / DRY / SOLID** — просто, без оверинжиниринга.
|
||||
- **≤200 строк на файл.** Фасад-делегат (`Renderer2D`, `Module` ≈1181) — исключение; больше логики → выноси `package-private` хелперы в тот же пакет.
|
||||
- **≤4 файла на папку** — больше → новая подпапка по смыслу.
|
||||
- **Никакого статика от Minecraft**: `Minecraft.getInstance()` только локальной переменной в месте использования, никогда в поле. `INSTANCE` — только для сервисов без lifecycle.
|
||||
- **Lifecycle гейты**: `Lifecycle.isActive()/canRunRender()/canRunHud()` — до `activate()` события/тики не работают; `EventBus.post()` сам гейтит.
|
||||
- **Никаких this-escape** — конструктор не публикует `this`.
|
||||
- **Никаких мёртвых миксинов** — миксин только если реально нужен; `cancellable=true` только при реальном `cancel`.
|
||||
- **`dev.loki.lovisual.mixins.*` — ТОЛЬКО классы с `@Mixin`.** Обычный хелпер там крашит `IllegalClassLoadError` (Knot). Хелперы — в `util/` или пакет фичи (см. `HandDynamics`).
|
||||
- **Без пустышек** — файл без реализации не создаётся.
|
||||
- **Каждый этап компилируется**: `./gradlew build` перед коммитом.
|
||||
- **Тесты обязательны** для новой чистой логики (JUnit 5, без моков Minecraft — выноси математику в `static` и тесть).
|
||||
- **Референсы (`ref/`)** — чужой код (SoupVisuals SOUP-1.0, Delta и др.): **только идеи**, пере-реализация под наши правила, никакого копирования.
|
||||
- **`System.currentTimeMillis()` в модулях → `GameClock.millis()`**, `static final Minecraft` в полях — баг.
|
||||
|
||||
---
|
||||
|
||||
## 3. Карта пакетов
|
||||
|
||||
```
|
||||
dev.loki.lovisual
|
||||
├── LoVisual.java — ClientModInitializer: весь старт (тики, рендер-фазы)
|
||||
├── Lifecycle.java — INIT → ACTIVE → SHUTDOWN; гейты isActive/canRunRender/canRunHud
|
||||
├── LoVisual.java — ClientModInitializer: старт (тики, рендер-фазы)
|
||||
├── Lifecycle.java — INIT → ACTIVE → SHUTDOWN; гейты
|
||||
├── events/ — EventBus (@EventHandler, приоритеты, гейт по Module)
|
||||
│ └── impl/ — GameTickEvent, PacketEvent, AttackEntityEvent, LightmapModifyEvent...
|
||||
├── config/ — MainConfig, SettingDef, .lvcfg (legacy .cbcfg читается)
|
||||
│ ├── values/ — ConfigValue: bool/num/mode/color/set/... (есть тесты)
|
||||
│ └── profile/ — профили (collect/apply/diff/binary codec)
|
||||
├── features/
|
||||
│ ├── module/ — СИСТЕМА МОДУЛЕЙ (главное для вас)
|
||||
│ │ ├── Module.java — базовый класс (1181 стр, фасад)
|
||||
│ │ ├── ModuleInfo — @annotation для модуля
|
||||
│ ├── module/ — СИСТЕМА МОДУЛЕЙ (главное)
|
||||
│ │ ├── Module.java — базовый класс
|
||||
│ │ ├── ModuleInfo — @annotation
|
||||
│ │ ├── ModuleManager — реестр + диспетчер фаз
|
||||
│ │ ├── ModuleAutoLoader — авто-обнаружение по аннотации (ClassGraph)
|
||||
│ │ ├── Modules — удобный доступ: Modules.get(Класс.class)
|
||||
│ │ ├── ModuleCategory — COMBAT/MOVEMENT/PLAYER/VISUALS/MISC
|
||||
│ │ ├── ModuleAutoLoader — ClassGraph по @ModuleInfo (no-args ctor → postInit)
|
||||
│ │ ├── Modules — Modules.get(Класс.class) / enabled(id)
|
||||
│ │ ├── ModuleCategory — COMBAT/MOVEMENT/PLAYER/VISUALS/MISC (легит-направление: читы удаляем)
|
||||
│ │ ├── HudPhase / WorldPhase — фазы рендера
|
||||
│ │ └── modules/{combat,player,visuals,misc}/
|
||||
│ ├── command/ — команды: @CommandInfo + ClientCommand
|
||||
│ ├── command/ — @CommandInfo + ClientCommand (ClassGraph, префикс %)
|
||||
│ ├── gui/
|
||||
│ │ ├── clickgui/ — экран настроек (Setting → виджет)
|
||||
│ │ └── hud/ — HUD-элементы
|
||||
│ │ ├── draggable/ — перетаскиваемые (Fps, Coords, ModuleList, TargetHud…)
|
||||
│ │ └── nondraggable/ — статичные (CustomBar, CustomHotbar, BetterChat…)
|
||||
│ ├── relations/ — друзья/враги/категории (CategoryService)
|
||||
│ ├── security/ — BackdoorProtection (SSRF/translate-защита)
|
||||
│ └── theme/ — темы: Theme.theme() → Themes.Theme (цвета)
|
||||
├── config/
|
||||
│ ├── values/ — типы настроек: ConfigValue (bool/num/mode/color…)
|
||||
│ ├── SettingDef.java — GUI-нейтральный дескриптор настройки
|
||||
│ └── common/CommonSettingSchemas — переиспользуемые схемы (i18n-ключи)
|
||||
├── events/
|
||||
│ ├── EventBus (Events.BUS) — шина событий
|
||||
│ ├── @EventHandler — аннотация хендлера
|
||||
│ └── impl/ — все события (см. §5)
|
||||
├── mixins/ — миксины + accessors (mixins/accessors/)
|
||||
├── addon/ — система аддонов (API v0)
|
||||
├── api/v0/ — публичный API аддонов (module/client/render/clickgui)
|
||||
├── render/
|
||||
│ ├── engine/
|
||||
│ │ ├── renderer/Renderer2D — весь 2D-рендер (прямоугольники, круги, текст, items)
|
||||
│ │ ├── renderer/Renderer3D — 3D (линии, меши, quads)
|
||||
│ │ ├── text/ — Fonts.renderer("Onest", Regular), TextRenderer
|
||||
│ │ ├── animation/AnimationUtility
|
||||
│ │ ├── pipeline/LoVisualRenderPipelines — готовые пайплайны (UI_COLORED и т.д.)
|
||||
│ │ └── uniform/MeshBuilder — построение мешей
|
||||
│ └── engine/color/RenderColor — ARGB
|
||||
└── util/ — куча хелперов (см. §8)
|
||||
│ │ └── nondraggable/ — статичные (CustomBar, BetterChat, BetterButtons, DynamicIsland…)
|
||||
│ ├── relations/ — друзья/враги (PlayerRelations, CategoryService)
|
||||
│ ├── security/ — BackdoorProtection (SSRF/translate)
|
||||
│ └── theme/ — Themes.Theme (windowBg/header/surface/accent...), Theme.theme()
|
||||
├── render/engine/ — Renderer2D/3D, пайплайны, шрифты, MeshBuilder
|
||||
│ ├── renderer/Renderer2D — 2D (quad/roundedRect/circle/line/item/svg/effect)
|
||||
│ ├── renderer/Renderer3D — 3D (line/quad/triangle)
|
||||
│ ├── text/ — Fonts.renderer("Onest", Regular), TextRenderer
|
||||
│ ├── animation/AnimationUtility
|
||||
│ ├── pipeline/LoVisualRenderPipelines — UI_COLORED, WORLD_COLORED, HAND_*, SHADER_ESP_*, POST_FX...
|
||||
│ └── uniform/MeshBuilder
|
||||
├── mixins/ — @Mixin-классы (mixins.json, LoVisualMixinPlugin)
|
||||
│ └── mixins/accessors/ — MinecraftAccessor, PlayerInventoryAccessor...
|
||||
│ └── mixininterface/ — IGuiGraphics, IEntity...
|
||||
├── addon/ + api/v0/ — система аддонов (LoVisualAddon, LoVisualModuleExtension, RenderCallback)
|
||||
└── util/ — хелперы: target, pvp, aiming, time/GameClock, input/KeyManager, screen, text, raycast...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Жизненный цикл и старт
|
||||
## 4. Жизненный цикл и старт
|
||||
|
||||
В `LoVisual.onInitializeClient()` по порядку:
|
||||
1. `LoVisualRenderEngineBootstrap.init()`, `PostProcessManager.register(...)`
|
||||
2. `MainConfig.get()`, `AccountConfig.get()`, `CommandManager.init()`, `MediaSessionService`
|
||||
3. `ModuleAutoLoader.load("dev.loki.lovisual.features.module.modules")` — ищет `@ModuleInfo`, `new` + `postInit()`
|
||||
4. `ModuleManager.loadAllModuleConfigs()` — применяет `.lvcfg`, включает `enabled`
|
||||
5. `HudElements.init()`, `StaticHudElementBootstrap.init()`
|
||||
6. `Events.BUS.register(...)` — глобальные сервисы (`RotationManager`, `PvpTracker`...)
|
||||
7. `Lifecycle.activate()` — после этого тики/события/рендер работают
|
||||
|
||||
1. `LoVisualRenderEngineBootstrap.init()`
|
||||
2. `MainConfig.get()`, `AccountConfig.get()`, `CommandManager.init()`
|
||||
3. `ModuleAutoLoader.load("dev.loki.lovisual.features.module.modules")` — сканирует пакет,
|
||||
ищет классы с `@ModuleInfo`, создаёт через no-args конструктор и зовёт `module.postInit()`.
|
||||
4. `ModuleManager.loadAllModuleConfigs()` — применяет сохранённые конфиги, включает enabled-модули.
|
||||
5. `HudElements.init()`, `StaticHudElementBootstrap.init()` — HUD-элементы.
|
||||
6. `Events.BUS.register(...)` — реестрируются глобальные сервисы.
|
||||
7. `Lifecycle.activate()` — после этого события/тики работают.
|
||||
|
||||
**Вывод:** чтобы добавить модуль — просто положи класс с `@ModuleInfo` в пакет
|
||||
`...modules.<категория>` и всё. Реестрировать руками не нужно.
|
||||
|
||||
`Lifecycle` — главный гейт: `Lifecycle.isActive()`, `canRunRender()`, `canRunHud()`.
|
||||
До `activate()` модули и события не работают.
|
||||
**Вывод:** новый модуль — просто класс с `@ModuleInfo` в `...modules.<категория>`; регистрировать руками не нужно.
|
||||
|
||||
---
|
||||
|
||||
## 3. Модуль — быстрый старт
|
||||
|
||||
Минимальный модуль (по образцу `FullBright`):
|
||||
## 5. Модуль — быстрый старт
|
||||
|
||||
```java
|
||||
package dev.loki.lovisual.features.module.modules.misc;
|
||||
|
||||
import dev.loki.lovisual.config.values.NumberValue;
|
||||
import dev.loki.lovisual.events.EventHandler;
|
||||
import dev.loki.lovisual.events.impl.GameTickEvent;
|
||||
import dev.loki.lovisual.features.module.Module;
|
||||
import dev.loki.lovisual.features.module.ModuleCategory;
|
||||
import dev.loki.lovisual.features.module.ModuleInfo;
|
||||
import dev.loki.lovisual.config.values.primitive.NumberValue;
|
||||
import dev.loki.lovisual.events.meta.EventHandler;
|
||||
import dev.loki.lovisual.events.impl.render.GameTickEvent;
|
||||
import dev.loki.lovisual.features.module.core.Module;
|
||||
import dev.loki.lovisual.features.module.core.ModuleCategory;
|
||||
import dev.loki.lovisual.features.module.core.ModuleInfo;
|
||||
|
||||
@ModuleInfo(
|
||||
id = "my_feature",
|
||||
displayName = "My Feature",
|
||||
aliases = {"mf"},
|
||||
category = ModuleCategory.MISC,
|
||||
enabledByDefault = false
|
||||
)
|
||||
@ModuleInfo(id = "my_feature", displayName = "My Feature", category = ModuleCategory.MISC)
|
||||
public class MyFeature extends Module {
|
||||
private final NumberValue<Integer> amount = num("myFeatureAmount", 3, 0, 20);
|
||||
|
||||
private final NumberValue<Integer> amount =
|
||||
num("myFeatureAmount", 3, 0, 20); // имя_конфига, дефолт, мин, макс
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
// вызывается при включении
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTick() {
|
||||
// каждый игровой тик, если модуль включён
|
||||
@Override public void onEnable() {}
|
||||
@Override public void onDisable() {}
|
||||
@Override public void onTick() {
|
||||
if (!isEnabled()) return;
|
||||
int a = amount.get();
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
if (mc.level == null) return;
|
||||
}
|
||||
@EventHandler private void onTick(GameTickEvent e) {}
|
||||
}
|
||||
```
|
||||
|
||||
### `@ModuleInfo`
|
||||
```java
|
||||
id = "имя" // нижний регистр, уникальный, это ключ конфига и команды
|
||||
displayName = "Имя" // показывается в UI; можно i18n-ключ
|
||||
aliases = {} // необязательно
|
||||
category = ModuleCategory.XXX
|
||||
enabledByDefault = false
|
||||
description = "" // или i18n-ключ "module.<id>.description"
|
||||
```
|
||||
`@ModuleInfo`: `id` (snake, уникальный, ключ конфига/команды), `displayName`, `aliases`, `category`, `enabledByDefault`, `description` (i18n).
|
||||
|
||||
### Автогенерируемые настройки модуля
|
||||
У каждого модуля уже есть (создаются в конструкторе `Module`):
|
||||
- `enabled` (BooleanValue) — состояние,
|
||||
- `bind` (KeyBindSetting, значение `KeyBindValue`) — клавиша,
|
||||
- `activation_source`,
|
||||
- `show_in_module_list`.
|
||||
Автогенерируемые поля `Module`: `enabled`/`bind`/`activation_source`/`show_in_module_list` (`postInit` → `SettingFactory.fromDefs` → `ModuleManager`).
|
||||
|
||||
`postInit()` вызывается автоматически лоадером: читает поля-настройки, делает
|
||||
`SettingFactory.fromDefs(...)`, регистрирует модуль в `ModuleManager`.
|
||||
|
||||
### Хуки жизненного цикла (переопределяемые)
|
||||
| Метод | Когда |
|
||||
|-------|-------|
|
||||
| `onEnable()` / `onDisable()` | переключение модуля |
|
||||
| `onTick()` | каждый игровой тик (в мире) |
|
||||
| `onFrame(float tickDelta)` | каждый кадр |
|
||||
| `onKey(int key, int action)` | нажатия клавиш |
|
||||
| `onRender2D(GuiGraphicsExtractor)` | vanilla HUD pass |
|
||||
| `onRenderHudEngine(Renderer2D, TextRenderer, ...)` | кастомный 2D-рендер (см. §6) |
|
||||
| `onRenderHudEngineForeground(...)` | 2D поверх всего |
|
||||
| `onRenderWorld(PoseStack, SubmitNodeCollector)` | 3D vanilla-style |
|
||||
| `onRenderWorldEngine(Renderer3D, Renderer3D, float)` | 3D через движок (см. §7) |
|
||||
| `getHudPhase()` / `getWorldPhase()` | фаза, в которой рендерить |
|
||||
|
||||
> `getHudPhase()`/`getWorldPhase()` по умолчанию `NONE` — модуль вообще не рендерится
|
||||
> диспетчером. Верните фазу, чтобы рендер-хуки вызывались (см. §6/§7).
|
||||
|
||||
### Управление состоянием
|
||||
- `isEnabled()`, `setEnabled(boolean)`, `setEnabled(boolean, ModuleActivationSource)`, `toggle()`
|
||||
- `isAvailable()` / `getAvailabilityReason()` — переопределите `getUnavailableReason()`
|
||||
- `name()`, `getDisplayName()`, `getAliases()`, `getCategory()`, `getDescription()`
|
||||
- `saveConfig()`, `getConfigValue(String)`, `getSettings()`
|
||||
|
||||
### Доступ к другим модулям
|
||||
```java
|
||||
import dev.loki.lovisual.features.module.Modules;
|
||||
|
||||
Freecam fc = Modules.get(Freecam.class); // null если не активен/не зарегистрирован
|
||||
boolean on = Modules.enabled("freecam"); // по id
|
||||
```
|
||||
`ModuleManager.get(String id)` / `ModuleManager.get(Class)` / `ModuleManager.require(Class)` тоже доступны.
|
||||
Хуки: `onEnable/onDisable/onTick/onFrame/onKey/onRender2D/onRenderHudEngine/onRenderWorldEngine`, `getHudPhase()/getWorldPhase()` (по умолч. `NONE` — не рендерится). Управление: `isEnabled()/toggle()/setEnabled()`, `isAvailable()`, `Modules.get(Freecam.class)`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Настройки (ConfigValue → Setting)
|
||||
## 6. Настройки (ConfigValue → SettingDef)
|
||||
|
||||
Базовый класс: `config/values/ConfigValue<T>` — `get()`, `set(v)`, `getName()`,
|
||||
`toJson()`, `fromJson(Object)`, `toDisplay()`.
|
||||
База `ConfigValue<T>`: `get()/set()/getName()/toJson()/fromJson()`.
|
||||
|
||||
Внутри модуля настройки создаются **хелперами** в `Module` (все `protected final`).
|
||||
Имя в конфиге и `settingId` (i18n) могут различаться.
|
||||
Хелперы в `Module`/`BaseHudElement` (`protected final`):
|
||||
|
||||
### Хелперы в Module / BaseHudElement
|
||||
| Хелпер | Тип | Пример |
|
||||
|--------|-----|--------|
|
||||
| `bool(name, def)` | `BooleanValue` | `bool("my_on", true)` |
|
||||
| `num(name, def, min, max)` | `NumberValue<Integer/Long/Float/Double>` | `num("my_num", 3, 0, 20)` |
|
||||
| `mode(name, def, options...)` | `ModeValue` (String) | `mode("my_mode", "A", "A", "B", "C")` |
|
||||
| `enumMode(name, defEnum)` / `enumSetting(name, defEnum, ...)` | `EnumValue<E>` | см. `Reach` |
|
||||
| `num(name, def, min, max)` | `NumberValue` | `num("my_num", 3, 0, 20)` |
|
||||
| `mode(name, def, opts...)` | `ModeValue` | `mode("my_mode", "A", "A","B")` |
|
||||
| `enumMode(name, defEnum)` | `EnumValue` | см. `Reach` |
|
||||
| `color(name, "#AARRGGBB")` | `RGBAColorValue` | `color("my_bg", "#F7343434")` |
|
||||
| `colorNoAlpha(name, "#RRGGBB")` | `RGBColorValue` | `colorNoAlpha("my_fg", "#FFFFFF")` |
|
||||
| `text(name, def)` | `StringValue` | `text("my_text", "")` |
|
||||
| `textList(name)` / `textList(name, pickerMode)` | `SetValue` (множество строк) | `textList("my_list")` |
|
||||
| `itemList(name)` | `ItemIdSetValue` (список предметов) | `itemList("my_items")` |
|
||||
| `group(name, defaultsMap)` | `BooleanMapValue` (группа чекбоксов) | `group("gui_sounds", Map.of(...))` |
|
||||
| `bind(name, def, mode)` | `KeyBindValue` | `bind("my_bind", "R", BindMode.PRESS)` |
|
||||
| `action(name, defaultKey, mode)` | `FunctionBindSetting` | см. ниже |
|
||||
| `setDefaultBind("KEY")` | — | клавиша модуля (см. `ClickGui`) |
|
||||
| `text/textList/itemList/group/bind/action` | `StringValue/SetValue/ItemIdSetValue/...` | `itemList("my_items")` |
|
||||
|
||||
**Варианты с общим i18n** (`*Common`): `boolCommon`, `numCommon`, `modeCommon`,
|
||||
`enumCommon`, `color` (без common) — берут схему из `CommonSettingSchemas`:
|
||||
`*Common` варианты (`boolCommon/numCommon/enumCommon`) берут схему из `CommonSettingSchemas`.
|
||||
|
||||
```java
|
||||
distance = numCommon("reachDistance", "range", CommonSettingSchemas.COMBAT_RANGE, 4.5, 3.0, 6.0);
|
||||
mode = enumCommon("reachMode", "mode", CommonSettingSchemas.ANTICHEAT_MODE, Mode.NORMAL, Mode.values());
|
||||
```
|
||||
Условность: `visibleWhen(numCommon(...), () -> cond)`, `appliesWhen(setting, () -> mc.player!=null, "Только в мире")`. Экшены: `action("climb","G", PRESS)` → `isActionPressedOnce("climb")`.
|
||||
|
||||
### Условная видимость / доступность
|
||||
```java
|
||||
private final NumberValue<Double> wallDistance =
|
||||
visibleWhen(numCommon("reachWallDistance", "wall_range", schema, 3.0, 0.0, 6.0),
|
||||
() -> raycast.get() == RaycastMode.THROUGH_WALLS);
|
||||
|
||||
// недоступен (показывается серым + причина):
|
||||
appliesWhen(mySetting, () -> mc.player != null, "Только в мире");
|
||||
```
|
||||
|
||||
### Экшены (вторая кнопка на модуль)
|
||||
```java
|
||||
action("climb", "G", BindMode.PRESS); // создаёт FunctionBindSetting
|
||||
|
||||
// в тике:
|
||||
if (isActionPressedOnce("climb")) { ... } // фронт нажатия
|
||||
if (isActionHeld("climb")) { ... } // удержание
|
||||
```
|
||||
|
||||
### Настройки в HUD-элементах
|
||||
У `BaseHudElement` те же хелперы (`bool`, `num`, `mode`, `enumSetting`, `color`,
|
||||
`textList`, `bind`, `group`, `visibleWhen`), но объявление идёт в
|
||||
`defineSettings(List<SettingDef>)` (см. §9).
|
||||
|
||||
### Виджеты в UI (SettingDef)
|
||||
`SettingDef` — GUI-нейтральный дескриптор. `SettingFactory.fromDef(def)` превращает
|
||||
его в виджет. Доступные `Kind`: `BOOLEAN, NUMBER, MODE, COLOR, COLOR_NO_ALPHA,
|
||||
TEXT, TEXT_LIST, COOLDOWN_RULES, PROTOCOL_HEURISTICS, GROUP, BIND`.
|
||||
Для модулей SettingDef строятся автоматически из хелперов — руками не нужно.
|
||||
`@DisableSettingI18n(name = false, options = true)` — отключить i18n у настройки.
|
||||
HUD-элементы объявляют настройки в `defineSettings(List<SettingDef>)`; модули — полями (автоматом). Виджеты `SettingDef.Kind`: `BOOLEAN, NUMBER, MODE, COLOR, TEXT, TEXT_LIST, GROUP, BIND...` (`@DisableSettingI18n`).
|
||||
|
||||
---
|
||||
|
||||
## 5. События (EventBus)
|
||||
## 7. HUD-элементы
|
||||
|
||||
`Events.BUS.post(event)` — доставка хендлерам. Хендлер — любой метод с одним
|
||||
аргументом-наследником `Event`, помеченный `@EventHandler(priority = N)`.
|
||||
Подписчики регистрируются автоматически при `ModuleManager.register()` (модули),
|
||||
или явно `Events.BUS.register(obj)` (в `LoVisual.onInitializeClient`).
|
||||
**Draggable** (`hud/draggable/impl/`, `@HudElementInfo(id, displayName, enabledByDefault, order)`, `DraggableHudElement`): `applyDefaultPosition(w,h)`, `renderEngine(Renderer2D, TextRenderer, GuiGraphicsExtractor, tickDelta, w,h)`, `usesEngineRenderer()=true`, `x/y/width/height`.
|
||||
|
||||
Модули-подписчики автоматически гейтятся: если модуль выключен — его хендлеры не вызываются.
|
||||
Поэтому в хендлерах модуля проверка `if (!isEnabled()) return;` не обязательна, но
|
||||
встречается в коде для ясности.
|
||||
|
||||
`Event` — база с `cancelled`/`cancel()`. Наследование событий поддерживается.
|
||||
|
||||
Полный список `events/impl/` (по имени файла):
|
||||
|
||||
```
|
||||
AttackEntityEvent BlinkPacketEvent CombatProtocolBossbarEvent
|
||||
CrosshairTargetUpdateEvent EventBreakBlock EventCollision EventPostSync
|
||||
EventPushOutOfBlocks EventSync EventTargetChanged FireworkEvent GameTickEvent
|
||||
I18nPreflightCollectEvent KeybindIsPressedEvent KeyInputEvent LightmapEvent
|
||||
LightmapModifyEvent MovementInputEvent PacketEvent PlayerJumpEvent PlayerMoveEvent
|
||||
PlayerSafeWalkEvent PlayerStepEvent PlayerStepSuccessEvent PlayerVelocityStrafe
|
||||
PostPlayerUpdateEvent PvpChatEvent PvpOverlayEvent PvpTabEvent
|
||||
RenderPrewarmCollectEvent RotationUpdateEvent SprintControlEvent
|
||||
```
|
||||
|
||||
Типичные события для нового модуля:
|
||||
- `GameTickEvent` — каждый тик,
|
||||
- `PacketEvent` — сетевые пакеты,
|
||||
- `AttackEntityEvent`, `EventTargetChanged` — бой/цели,
|
||||
- `LightmapModifyEvent` — свет (пример `FullBright`),
|
||||
- `PlayerMoveEvent`, `MovementInputEvent` — движение,
|
||||
- `RenderPrewarmCollectEvent` — «прогреть» шрифты/текстуры для вашего рендера.
|
||||
|
||||
Пример:
|
||||
```java
|
||||
@EventHandler
|
||||
private void onTick(GameTickEvent event) { ... }
|
||||
```
|
||||
**Static** (`hud/nondraggable/impl/`, `@HudElementRegister(order)`, `AbstractHudElement` `super(id,"Title",defaultEnabled)` + `INSTANCE`): для вкладки UI добавить `new StaticHudCard(...)` в `MenuSettingsResolver.STATIC_UI_CARDS`. Тема: `Theme.theme().accent()/windowBg()`, `HudRenderUtil.setAlpha/mixColor`, текст `Fonts.renderer(...)`.
|
||||
|
||||
---
|
||||
|
||||
## 6. 2D-рендер (HUD)
|
||||
## 8. Команды
|
||||
|
||||
### Фазы
|
||||
Верните не-`NONE` фазу из `getHudPhase()`:
|
||||
```java
|
||||
HudPhase: FIRST, BEFORE_MISC_OVERLAYS, AFTER_MISC_OVERLAYS, AFTER_BOSS_BAR,
|
||||
BEFORE_DEMO_TIMER, BEFORE_CHAT, AFTER_SUBTITLES, LAST
|
||||
```
|
||||
`ModuleManager.renderHudEngine(phase, renderer, textRenderer, ctx, tickDelta)` вызывает
|
||||
у модуля `onRenderHudEngine(...)`; `renderHudEngineForeground` → `onRenderHudEngineForeground`.
|
||||
|
||||
### Renderer2D — главный 2D API
|
||||
Включается/выключается через `begin()` / `render()` (уже делает диспетчер).
|
||||
Готовые методы (фасад `Renderer2D.java`, все в `px`):
|
||||
|
||||
- **Фигуры:** `quad(x,y,w,h,argb)`, `quad(x,y,w,h,cTL,cTR,cBR,cBL)`,
|
||||
`roundedRect(x,y,w,h,radius,argb)`, `roundedRectStroke(...)`,
|
||||
`roundedRectGradient(...)`, `roundedRectGlow/Shadow/SoftShadow(...)`,
|
||||
`circle(cx,cy,r,argb)`, `circleStroke(...)`, `arcStroke(...)`,
|
||||
`line(x1,y1,x2,y2,argb)`, `boxLines(...)`, `roundedSmokeFill(...)`.
|
||||
- **Текст:** см. §6.2.
|
||||
- **Предметы:** `item(ItemStack, x, y, scale)`, `itemUnscaled(...)`, `itemPivot(...)`.
|
||||
- **SVG/текстуры:** `svg(Identifier/Path, x, y, w, h)`, `textureQuad(...)`, `msdfTextureQuad(...)`.
|
||||
- **Effect (стеклянные панели):** `effect(UiEffectSpec)`.
|
||||
|
||||
### Текст
|
||||
```java
|
||||
TextRenderer tr = Fonts.renderer("Onest", FontInfo.Type.Regular, textRenderer);
|
||||
// или Fonts.renderer("OnestMedium", Regular), "OnestBold", "Inter", "Iosevka", "Icons"...
|
||||
tr.begin(scale, true, false);
|
||||
double w = tr.getWidth("text", false);
|
||||
double h = tr.getHeight(false);
|
||||
tr.render("text", x, y, new RenderColor(0xFFFFFFFF), false);
|
||||
tr.end();
|
||||
```
|
||||
Универсальная запись «в одном блоке»:
|
||||
```java
|
||||
textRenderer.begin(scale, true, false);
|
||||
textRenderer.render("FPS", x, y, new RenderColor(argb), false);
|
||||
textRenderer.end();
|
||||
```
|
||||
`TextRenderer.get()` — текущий рендерер (передаётся в хук). `HudScale.scale(screenW, screenH)`
|
||||
даёт масштаб для адаптивного HUD (см. `Fps`).
|
||||
|
||||
### Иконки / текстуры
|
||||
Идентификаторы: `Identifier.fromNamespaceAndPath("lovisual", "textures/hud/elements/fps.png")`.
|
||||
SVG лежат в `assets/lovisual/svg/`.
|
||||
|
||||
### Анимации
|
||||
`AnimationUtility`: `approach(value, target, dt, speed)`, `fast(...)`, `lerp(...)`,
|
||||
`easeOutCubic`, `easeInOutCubic`, `easeOutBack`, `blink(ms)`, `deltaTime()`, `snap(...)`.
|
||||
|
||||
### Хелперы
|
||||
- `HudRenderUtil`: `setAlpha(argb, a)`, `mixColor(a, b, t)`, `glassBackground()`,
|
||||
`drawLiquidGlass(...)`, `animateVisibility(...)`, `visibilityScale(...)`.
|
||||
- `HudGlobalConfig.get()`: `getFontSize()`, `getBlurRadius()` и т.д.
|
||||
- Тема: `Theme.theme()` (статический импорт `dev.loki.lovisual.features.theme.Theme.theme`)
|
||||
→ `Themes.Theme` c полями `windowBg, windowHeader, windowStroke, surface, surfaceHover,
|
||||
cardEnabled, cardDisabled, textPrimary, textMuted, accent, accentSoft, strokeSoft`
|
||||
(всё `int` ARGB). См. `Fps.updatePalette()`.
|
||||
|
||||
---
|
||||
|
||||
## 7. 3D-рендер (мир)
|
||||
|
||||
### Фазы
|
||||
Верните не-`NONE` фазу из `getWorldPhase()`:
|
||||
```java
|
||||
WorldPhase: BEFORE_ENTITIES, AFTER_ENTITIES, BEFORE_TRANSLUCENT, END_MAIN, AFTER_POST_PROCESS
|
||||
```
|
||||
|
||||
### Renderer3D — простой путь
|
||||
В хуке `onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta)`:
|
||||
`ClientCommand` + `@CommandInfo(id, aliases, usage, descriptionKey)`, регистрация в `CommandManager` (ClassGraph). Префикс **`%`** (legacy `@`). `CommandPrefix` — источник истины; автокомплит `suggest()` + `UsageHints`.
|
||||
|
||||
```java
|
||||
renderer.begin();
|
||||
renderer.line(x1, y1, z1, x2, y2, z2, r, g, b, a); // линия
|
||||
renderer.quad(x1,y1,z1, x2,y2,z2, x3,y3,z3, x4,y4,z4); // квад
|
||||
renderer.triangle(...);
|
||||
renderer.render(new PoseStack()); // в конце
|
||||
```
|
||||
Пример: `Tracers.onRenderWorldEngine` (линии от камеры к игрокам).
|
||||
|
||||
### MeshBuilder + RenderPipeline — продвинутый путь
|
||||
```java
|
||||
MeshBuilder mesh = new MeshBuilder(LoVisualRenderPipelines.UI_TEXTURED_ADDITIVE);
|
||||
mesh.begin();
|
||||
mesh.ensureQuadCapacity();
|
||||
int i1 = mesh.vec2(x, y).vec2(u, v).color(r, g, b, a).next();
|
||||
int i2 = mesh.vec2(...).vec2(...).color(...).next();
|
||||
mesh.quad(i1, i2, i3, i4);
|
||||
mesh.end();
|
||||
```
|
||||
Готовые пайплайны в `LoVisualRenderPipelines` (фрагмент):
|
||||
```
|
||||
UI_COLORED, UI_COLORED_LINES, UI_TEXTURED, UI_TEXTURED_ADDITIVE, UI_TEXT, UI_TEXT_MSDF,
|
||||
UI_SVG_MSDF, UI_BLUR, WORLD_COLORED, WORLD_COLORED_LINES, WORLD_COLORED_DEPTH,
|
||||
WORLD_TEXTURED, WORLD_TEXTURED_DEPTH, WORLD_TEXT, WORLD_TEXT_MSDF, WORLD_DECAL_SDF,
|
||||
WORLD_COLORED_LINES_DEPTH, WORLD_WIDE_COLORED_LINES, ...HAND_*, SHADER_ESP_*, POST_FX...
|
||||
```
|
||||
Отправить на отрисовку (2D-стрелки из `Tracers`):
|
||||
```java
|
||||
MeshRenderer.begin()
|
||||
.attachments(mc.gameRenderer.mainRenderTarget())
|
||||
.pipeline(LoVisualRenderPipelines.UI_TEXTURED_ADDITIVE)
|
||||
.mesh(mesh).transform(matrix4f)
|
||||
.sampler("u_Texture", view, sampler)
|
||||
.end();
|
||||
```
|
||||
`Renderer3D.Cull` — утилиты отсечения: `isInFrustum(...)`, `isSectionVisible(...)`, `isInFront(...)`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Полезные утилиты (`util/`)
|
||||
|
||||
| Пакет / класс | Что даёт |
|
||||
|---------------|----------|
|
||||
| `util.target.TargetManager` | текущая цель: `getTarget()`, `getTarget(true/false)`, `onAttack()`, `setPredictionTarget()`, `Source` |
|
||||
| `util.pvp.PvpTracker`, `PvpTargetTracker` | статистика PvP / отслеживание цели |
|
||||
| `util.aiming.RotationManager` | плавные/снап-ротации |
|
||||
| `util.combat.VulcanReachController` | клампинг дистанции (режим античита) |
|
||||
| `util.time.GameClock` | `millis()` — игровое время (тики×50мс), вне мира — стенное |
|
||||
| `util.time.TimerController` | таймеры |
|
||||
| `util.input.KeyManager` | `wasPressed("func")`, `isHeld("func")`, `isComboHeldAllowScreen(...)` |
|
||||
| `util.screen.ClientScreen` | текущий экран: `ClientScreen.current(mc)` / `.show(mc, screen)` |
|
||||
| `util.text` | текст-хелперы |
|
||||
| `util.entity.simulation.PlayerSimulationCache`, `BoatSimulationCache` | предсказание позиций |
|
||||
| `util.raycast` | рейкасты |
|
||||
| `util.item.OmniItemUtils` | утилиты предметов |
|
||||
| `util.combat.protocol.CombatProtocolHeuristics` | эвристики античита |
|
||||
| `util.logging.DebugLog` | `DebugLog.info/warn/error/config(...)` (printf-style) |
|
||||
| `util.resources.RenderResourceReadiness` | готовность рендер-ресурсов |
|
||||
| `features.relations.CategoryService` | `isFriend(Player)`, `getColor(Player)` — категории игроков |
|
||||
| `util.FastFps` | `getFps()` — быстрый FPS без накладок |
|
||||
|
||||
Ленивый доступ к MC:
|
||||
```java
|
||||
private final Minecraft mc = Minecraft.getInstance();
|
||||
```
|
||||
(поле, но это ок — поле инициализируется при создании модуля, а модули создаются
|
||||
в рантайме; правило «никакого `static final Minecraft` в полях»).
|
||||
|
||||
---
|
||||
|
||||
## 9. HUD-элементы
|
||||
|
||||
### Draggable (перетаскиваемый) — пример `Fps`
|
||||
```java
|
||||
@HudElementInfo(id = "fps", displayName = "FPS", enabledByDefault = true, order = 120)
|
||||
public final class Fps extends DraggableHudElement {
|
||||
// настройки — прямо поля:
|
||||
private final NumberValue<Double> scale = num("fps_scale", 2.37, 0.5, 5.0);
|
||||
private final RGBColorValue iconColor = visibleWhen(colorNoAlpha("fps_icon_color", "#FFFFFF"), this::isCustomMode);
|
||||
|
||||
@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) {
|
||||
if (!preview && !isEnabled()) { width = 0f; height = 0f; return; }
|
||||
// считаем width/height, рисуем через renderer + Fonts.renderer(...)
|
||||
}
|
||||
}
|
||||
```
|
||||
- Настройки объявляются как поля с хелперами (автоматом попадают в SettingDef).
|
||||
- `defaultLayout(x, y, anchorX, anchorY)` / `defaultLinkedLayout(...)` — стартовое положение.
|
||||
- `x, y, width, height` — позиция и размер (обновляйте width/height в renderEngine!).
|
||||
- Регистрация автоматическая (сканирование по `@HudElementInfo`).
|
||||
- Интерактив: `isMouseOverInteractive(mx, my)`, `onMouseClicked(mx, my, button)`.
|
||||
|
||||
### Nondraggable (статичный)
|
||||
Классы в `features/gui/hud/nondraggable/impl/` (CustomBar, CustomHealthBar, CustomHotbar,
|
||||
BetterTooltips, BetterButtons, DynamicIsland). Наследуют `BaseHudElement`, рендер через
|
||||
`renderEngine(...)` + `getRenderSpace()`/`getHudPhase()`.
|
||||
|
||||
---
|
||||
|
||||
## 10. Команды
|
||||
|
||||
Интерфейс `ClientCommand` + аннотация `@CommandInfo`, регистрация — вручную в `CommandManager`.
|
||||
|
||||
Префикс команд в чате — **`%`** (старый `@` тоже принимается). Источник истины —
|
||||
`CommandPrefix` (`CHAR`/`STRING`/`isCommandLike`/`strip`). Автокомплит: имена команд +
|
||||
аргументы из `suggest()`, а если команда не дала вариантов — подсказка-usage через
|
||||
`UsageHints` (например `%toggle <module>`).
|
||||
|
||||
```java
|
||||
@CommandInfo(
|
||||
id = "toggle",
|
||||
aliases = "t",
|
||||
usage = "%toggle <module> [on|off|toggle]",
|
||||
descriptionKey = "command.toggle.description"
|
||||
)
|
||||
@CommandInfo(id="toggle", aliases="t", usage="%toggle <module> [on|off]", descriptionKey="command.toggle.description")
|
||||
public final class ToggleCommand implements ClientCommand {
|
||||
@Override
|
||||
public boolean execute(CommandContext ctx) {
|
||||
// ctx.arg(0), ctx.arg(1), ...
|
||||
CommandOutput.success("Done");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> suggest(CommandContext ctx, int argIndex, String token) {
|
||||
return List.of();
|
||||
}
|
||||
public boolean execute(CommandContext ctx){ CommandOutput.success("Done"); return true; }
|
||||
public List<String> suggest(CommandContext ctx,int i,String token){ return List.of(); }
|
||||
}
|
||||
```
|
||||
- `CommandContext(mc, raw, name, args)` — `arg(i)` возвращает null если нет.
|
||||
- `CommandOutput` — `success/warning/error/send(String)` с префиксом `[LoVisual]`.
|
||||
- `CommandManager.init()` регистрирует команды из `impl/` (через ClassGraph, как модули).
|
||||
|
||||
`CommandContext.arg(i)`, `CommandOutput.success/warning/error`.
|
||||
|
||||
---
|
||||
|
||||
## 11. API для аддонов (`api/v0/`)
|
||||
## 9. События (EventBus)
|
||||
|
||||
- `LoVisualAddon` — entrypoint `"lovisual:addon"`: `onConfigureModules`,
|
||||
`onInitialize(LoVisualAddonContext)`, `onClientReady`, `onShutdown`.
|
||||
- `LoVisualModuleExtension` — расширение модуля (`ModuleExtensionContext`),
|
||||
перехватывает `beforeEnable/afterEnable/beforeTick/...` (см. `ModuleExtensionManager`).
|
||||
- `LoVisualClientApi.get()` — `modules()`, `module(id)`, `isModuleEnabled(id)`, `setModuleEnabled`.
|
||||
- `render/` — `LoVisualRenderCallback` + `LoVisualRenderStage`
|
||||
(HUD_RAW, HUD_SCALED, HUD_LOGICAL, WORLD_*, SCREEN_*), `LoVisualPostProcessCallback`,
|
||||
`LoVisualUniforms`, `LoVisualRenderPipelineBuilder`.
|
||||
- `clickgui/` — `LoVisualClickGuiSection`, `LoVisualClickGuiRenderContext` — свои секции в кликгую.
|
||||
`Events.BUS.post(event)` → `@EventHandler(priority=N)` (один аргумент-`Event`). Модули гейтятся `isEnabled()`. `Event` имеет `cancel()`.
|
||||
|
||||
`events/impl/` : `GameTickEvent`, `PacketEvent`, `AttackEntityEvent`, `EventBreakBlock`, `LightmapModifyEvent`, `PlayerMoveEvent`, `RenderPrewarmCollectEvent`...
|
||||
|
||||
```java
|
||||
@EventHandler private void onTick(GameTickEvent e){ }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. i18n (языковые ключи)
|
||||
## 10. 2D-рендер (HUD)
|
||||
|
||||
Файлы: `src/main/resources/assets/lovisual/lang/en_us.json`, `ru_ru.json`.
|
||||
Фазы `getHudPhase()`: `FIRST, BEFORE_MISC_OVERLAYS, AFTER_MISC_OVERLAYS, AFTER_BOSS_BAR, BEFORE_DEMO_TIMER, BEFORE_CHAT, AFTER_SUBTITLES, LAST` → `onRenderHudEngine`.
|
||||
|
||||
Соглашения по ключам:
|
||||
- Модуль: `module.<id>`, `module.<id>.name`, `module.<id>.description`,
|
||||
настройка: `module.<id>.setting.<setting_id>`, опции: `module.<id>.option.<option>`.
|
||||
- HUD draggable: `hud.draggable.<id>` / `setting.draggable.<id>.<setting>`.
|
||||
- HUD static: `hud.static.<id>`.
|
||||
- Команды: `command.<id>.description`.
|
||||
- Общие схемы: `CommonSettingSchemas` указывают `commonI18nKeys` (например `combat.range`)
|
||||
— они ищутся в lang-файлах с префиксом `setting.common.<key>` (проверьте по `CommonKey`).
|
||||
|
||||
`I18n.get("key")` — получение перевода. Если ключа нет — возвращается сам ключ.
|
||||
В `Module.getDisplayName()`/`getDescription()` fallback на `displayName`/`description`.
|
||||
`Renderer2D` (фасад, `begin()/render()` делает диспетчер): `quad/roundedRect/roundedRectGradient/circle/line/item/svg/effect`. Текст: `Fonts.renderer("Onest", Regular, textRenderer).begin(scale,true,false) → getWidth/render → end()` или `textRenderer.begin...`. Анимации `AnimationUtility`, хелперы `HudRenderUtil`/`HudGlobalConfig`/`Theme.theme()`.
|
||||
|
||||
---
|
||||
|
||||
## 13. Миксины и accessors
|
||||
## 11. 3D-рендер (мир)
|
||||
|
||||
- Все миксины в `mixins/` (`@Mixin(...)`). Пакеты: `accessors`, `iris`, `sodium`, `moreculling`,
|
||||
`xaero`, `security`.
|
||||
- Accessors в `mixins/accessors/` — примеры: `MinecraftAccessor`, `LocalPlayerAccessor`,
|
||||
`PlayerInventoryAccessor`, `LivingEntityAccessor`, `EntityAccessor`, `ItemCooldownManagerAccessor`.
|
||||
- Реестр: `resources/lovisual.mixins.json`. `LoVisualMixinPlugin` — условная загрузка.
|
||||
- Правило: «никаких мёртвых миксинов» — добавляйте миксин только если реально нужен.
|
||||
- `mixininterface/` — интерфейсы-протуберанцы для доступа к данным из миксинов (`IGuiGraphics`, `IEntity`).
|
||||
Фазы `getWorldPhase()`: `BEFORE_ENTITIES, AFTER_ENTITIES, BEFORE_TRANSLUCENT, END_MAIN, AFTER_POST_PROCESS` → `onRenderWorldEngine(Renderer3D, Renderer3D, tickDelta)`.
|
||||
|
||||
Простой путь: `renderer.begin(); renderer.line(...); renderer.quad(...); renderer.render(new PoseStack());` (см. `Tracers`). Продвинутый: `MeshBuilder` + `LoVisualRenderPipelines` (`UI_COLORED, WORLD_COLORED...`). `Renderer3D.Cull.isInFrustum`.
|
||||
|
||||
---
|
||||
|
||||
## 14. Чек-лист нового модуля
|
||||
## 12. Миксины и accessors
|
||||
|
||||
1. `@ModuleInfo` на классе в `features/module/modules/<категория>/`.
|
||||
2. Унаследовать `Module`, переопределить `onEnable/onDisable/onTick` и т.д.
|
||||
3. Настройки — поля с хелперами (`num`, `bool`, `mode`, `color`, `visibleWhen`…).
|
||||
4. Если нужен рендер — вернуть `getHudPhase()`/`getWorldPhase()` и реализовать
|
||||
`onRenderHudEngine`/`onRenderWorldEngine`.
|
||||
5. Если нужны события — методы с `@EventHandler`.
|
||||
6. `i18n`: добавить `module.<id>.name/description` и ключи настроек в `en_us.json`/`ru_ru.json`.
|
||||
7. Проверки: `./gradlew build` (сборка), `./gradlew test` (тесты), для чистой логики — JUnit.
|
||||
8. Файл ≤200 строк — иначе вынести логику в хелпер-классы в том же пакете.
|
||||
Все в `mixins/` (`@Mixin`), реестр `lovisual.mixins.json` (`LoVisualMixinPlugin`). Accessors в `mixins/accessors/` (`PlayerInventoryAccessor`...), `mixininterface/` (`IGuiGraphics`). Правило: только `@Mixin` в `mixins.*`.
|
||||
|
||||
---
|
||||
|
||||
## 13. Конфиг, профили и i18n
|
||||
|
||||
Модули/HUD → `<game>/lovisual/*.lvcfg` (legacy `.cbcfg` читается). Профили `config/profile/` (ClickGUI → Configs).
|
||||
|
||||
i18n `assets/lovisual/lang/{en_us,ru_ru}.json`:
|
||||
- модуль `module.<id>.description`, `setting.<id>.<setting_id>`, `option.<Option>`
|
||||
- draggable `hud.draggable.<id>`, static `setting.hud_element.<id>.<key>`, команды `command.<id>.description`, общие `setting.common.<key>`
|
||||
|
||||
Пропущенный ключ ловит `MissingI18nReporter` в dev.
|
||||
|
||||
---
|
||||
|
||||
## 14. Полезные утилиты (`util/`)
|
||||
|
||||
`TargetManager`, `PvpTracker`, `RotationManager`, `GameClock.millis()`, `TimerController`, `KeyManager`, `ClientScreen`, `PlayerSimulationCache`, `OmniItemUtils`, `DebugLog`, `RenderResourceReadiness`, `CategoryService`, `FastFps`...
|
||||
|
||||
Ленивый `Minecraft.getInstance()` — только локально.
|
||||
|
||||
---
|
||||
|
||||
## 15. API для аддонов (`api/v0/`)
|
||||
|
||||
`LoVisualAddon` (`onConfigureModules/onInitialize/onClientReady/onShutdown`), `LoVisualModuleExtension` (`beforeEnable/afterEnable/beforeTick...`), `LoVisualClientApi.get().modules()/isModuleEnabled()`, `LoVisualRenderCallback` (`HUD_RAW, WORLD_...`), `LoVisualClickGuiSection`.
|
||||
|
||||
---
|
||||
|
||||
## 16. Тесты
|
||||
|
||||
`src/test/java/` зеркалит `main`, **только чистая логика** (без MC рантайма): `util/text/NameProtectorTest`, `config/values/*Test`, `config/profile/ConfigProfileValueCodecTest`, `events/EventBusTest` (нужен `Lifecycle.activate()`).
|
||||
|
||||
Выноси `static int tint(...)` / `record Layout(...)` и тесть.
|
||||
|
||||
---
|
||||
|
||||
## 17. Чек-лист нового модуля / перед коммитом
|
||||
|
||||
1. `@ModuleInfo` в `modules/<категория>/`, `extends Module`, `onEnable/onTick` + `@EventHandler` если нужно
|
||||
2. Настройки хелперами (`num/bool/color/visibleWhen`), `i18n` в оба `lang` файла
|
||||
3. Рендер: верни `getHudPhase()/getWorldPhase()` + `onRenderHudEngine/onRenderWorldEngine`
|
||||
4. `./gradlew build` зелёный, `./gradlew test` зелёный, файл ≤200, папка ≤4, нет `static Minecraft` в полях, нет `System.currentTimeMillis()` → `GameClock`, нет пустышек/мёртвых миксинов, `mixin` только `@Mixin`
|
||||
5. `TODO.md` отмечен, `ref/` не копировался
|
||||
|
||||
**Не делай:** `static final Minecraft` в полях, `this`-escape, мёртвый код, копипаст чужого `ref/`.
|
||||
|
||||
---
|
||||
|
||||
## 18. Современные рекомендации (2025-2026, web-поиск)
|
||||
|
||||
### Clean Code — без догматизма
|
||||
- **YAGNI** «You Aren't Gonna Need It» — не пиши на будущее; **KISS** > OCP в быстро меняющемся коде. Принцип — эвристика, не закон.
|
||||
- **DRY ловушка**: дублирование дешевле неверной абстракции (Sandi Metz). Выноси только *семантически* одинаковое, синтаксически похожее `for`-циклы — не повод для хелпера → лишняя связность.
|
||||
- **SOLID критика**: ортодоксальный SOLID плодит `Service → RepoInterface → RepoImpl → QueryBuilder`, низкая связность, но нечитаемо и 15× медленнее (Casey Muratori «Clean Code, Horrible Performance» 2023: замена полиморфного OCP на `switch` дала 15×). Применяй умеренно, только где абстракция реально нужна.
|
||||
- **Читаемость**: без глубокой вложенности — guard-clauses `if (user==null) return;`, без магических чисел → `static final MIN_DRINKING_AGE=21`, говорящие имена `customerOrderHistoryList` > `list`, комментарии *почему*, а не *что*, консистентное форматирование, DI вместо `new`.
|
||||
|
||||
### Java 21/25 (LTS)
|
||||
- `if (obj instanceof String s)` / `record Point(int x,int y)` + `if (o instanceof Point(int x,int y))` / `switch` с pattern-matching и `case null`, `sealed interface Command permits Get,Put,Delete` — меньше кастов, безопаснее API.
|
||||
- **Virtual threads** — не «pixie dust»; дают выигрыш на I/O-bound, но не на CPU-bound. **ScopedValue** (финал JDK25) + **Structured Concurrency** (preview JDK25) лучше `ThreadLocal` по памяти/синхронизации, особенно с виртуальными потоками.
|
||||
- **JDK25 perf**: `String.hash` `@Stable` → константная свёртка `Map.of("constant",...)`, `Stable Value` preview, `ForkJoinPool` теперь `ScheduledExecutorService` (быстрее отмена timeout), ML-KEM/ML-DSA квантостойкая криптография ×2 на AVX-512/ARM, `HKDF` и др. — просто обнови JDK, код ускорится без изменений.
|
||||
|
||||
### Secure Coding (OWASP Java Cheat Sheet)
|
||||
- **Injection**: только параметризация (`PreparedStatement`, JPQL `setParameter`), не конкатенация строк; для `isReachable` вместо `Runtime.exec("ping "+host)`.
|
||||
- **XSS**: `OWASP Java HTML Sanitizer` + `OWASP Java Encoder` (`Encode.forHtml`) при выводе к пользователю.
|
||||
- **Крипто**: никогда не пиши своё — используй JCA/JCE, `AES-GCM` + `ECDH`, каждый раз новый `nonce`, секреты — через cloud secret manager, ключи ротируй. Слабые `MD5/DES` запрещены `JEP 565` в Java 21.
|
||||
|
||||
### Fabric / Mixin (2025-2026)
|
||||
- Порядок предпочтения: `@Inject` (callback, стекается) > `MixinExtras @WrapOperation/@ModifyExpressionValue` > `@Redirect/@ModifyConstant` (только один на таргет → конфликт) > `@Overwrite` (почти никогда).
|
||||
- `@Unique modid$field`, абстрактный mixin-класс (не надо имплементить интерфейсы родителя), `@Shadow` для доступа, `(Target)(Object)this` для `this`, внутренние классы `Outer$Inner`.
|
||||
- Сеть: пакеты приходят на net-thread — парсь там, а доступ к `Minecraft`/`Level` только через `client.execute` / task-queue на main thread.
|
||||
- Избегай `java.awt/javax.swing` — вешает игру; не патчи Fabric API напрямую, ищи `FabricBlockSettings` и т.п.
|
||||
|
||||
*Источники: Medium 2025 DRY/KISS/YAGNI, Baeldung Clean Code, JavaCodeGeeks Dark Side SOLID, Oracle Inside Java 2025-2026, Fabric Wiki mixin_tips, OWASP Java Security Cheat Sheet.*
|
||||
|
||||
**Не делайте:**
|
||||
- `static final Minecraft` в полях → только ленивый `Minecraft.getInstance()`.
|
||||
- `System.currentTimeMillis()` в модулях → `GameClock.millis()`.
|
||||
- this-escape в конструкторе, `#[allow(dead_code)]`-аналогов (мёртвый код удаляется),
|
||||
мёртвые миксины.
|
||||
|
|
|
|||
|
|
@ -491,3 +491,35 @@
|
|||
HandDynamics.apply/ItemInHandRendererMixin. `./gradlew build`+`test` зелёные.
|
||||
|
||||
### Проверка после каждого пункта: `./gradlew build` ✓, `./gradlew test` ✓, TODO.md отмечен
|
||||
|
||||
## Фаза 11: Аудит attack-визуалов — sacura 1.21.4 / GodWeer-26.2 (2026-09-22)
|
||||
> Референсы распакованы в `ref/clients/sacura-1.21.4` и `ref/clients/godweer-26.2`.
|
||||
> Вывод по большинству эффектов: у нас уже полноценнее (больше режимов, единый render
|
||||
> engine с пайплайнами вместо сырого GL). Ниже — конкретные пробелы, которые реально
|
||||
> стоит доделать; всё остальное сравнённое (HitEffect, KillEffect, JumpCircles,
|
||||
> Hitboxes, ItemPhysic, ViewModel offsets) уже на уровне или выше обоих референсов.
|
||||
|
||||
- [ ] **ViewModel — `Custom` swing-режим.** sacura (`SwingAnimationsModule`) имеет режим
|
||||
"Кастомная" с explicit слайдерами угол/наклон. У нас 8 фиксированных пресетов
|
||||
(Hammer/Chop/Arc/Double/Shake/Shove/Slash/Thrust), но нет свободно настраиваемого.
|
||||
Также посмотреть на sacura-режимы "Возвращение"/"Возвращение к себе"/"Круговая" —
|
||||
возможно стоят портирования как доп. пресеты, если визуально отличаются от наших.
|
||||
- [ ] **HitEffect — опциональный distortion/glow post-process слой.** godweer
|
||||
(`HitEffectRenderer`) кроме частиц кладёт screen-space distortion+glow слайдеры.
|
||||
У нас частицы (Bubbles/Wave/Impact) есть, post-process слоя над ними нет.
|
||||
Паттерн `PostProcessPass` уже есть (см. `TotemFX`), можно переиспользовать —
|
||||
но не блокирующая, это чистый апгрейд поверх уже более богатой системы.
|
||||
- [ ] **JumpCircles — easing на появление/исчезновение.** sacura использует `EaseBackIn`
|
||||
для пружинки при спавне/затухании круга. Проверить `render/engine/math` — если
|
||||
easing-утилита уже есть, подключить к текущему fade вместо линейного.
|
||||
- [ ] **KillEffect — through-walls поведение.** godweer (`PopEffect`) имеет явный toggle
|
||||
"сквозь стены" для beam/взрыв эффектов. Проверить DepthMode у Lightning/Orthodox
|
||||
beam в нашем KillEffect — сейчас режется окклюзией или нет; если режется и это не
|
||||
специально — добавить toggle по аналогии.
|
||||
- [ ] **Trails — живое сравнение с sacura `TrailsModule`.** Не разбирался по коду в этом
|
||||
заходе (визуальный эффект, решение лучше принимать посмотрев обе стороны вживую
|
||||
в игре, а не по исходникам).
|
||||
|
||||
### Итог: ничего из sacura/GodWeer не даёт принципиально нового attack-эффекта,
|
||||
### которого нет в LoVisual — расхождение только в деталях полировки (easing-кривые,
|
||||
### доп. custom-режим свинга, возможный distortion-слой, through-walls toggle).
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
plugins {
|
||||
id 'net.fabricmc.fabric-loom' version '1.17.12'
|
||||
id 'maven-publish'
|
||||
id 'pmd'
|
||||
id 'checkstyle'
|
||||
id 'com.github.spotbugs' version '6.5.11'
|
||||
}
|
||||
|
||||
repositories {
|
||||
|
|
@ -117,7 +120,7 @@ dependencies {
|
|||
annotationProcessor 'org.projectlombok:lombok:1.18.46'
|
||||
testCompileOnly 'org.projectlombok:lombok:1.18.46'
|
||||
testAnnotationProcessor 'org.projectlombok:lombok:1.18.46'
|
||||
testImplementation platform('org.junit:junit-bom:5.12.2')
|
||||
testImplementation platform('org.junit:junit-bom:5.14.4')
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter'
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
devCompileOnly 'org.projectlombok:lombok:1.18.46'
|
||||
|
|
@ -162,12 +165,12 @@ dependencies {
|
|||
bundledLibrary "com.github.hypfvieh:dbus-java-transport-native-unixsocket:4.3.1"
|
||||
|
||||
// Provided by Minecraft runtime; keep it out of the bundled jar.
|
||||
compileOnly 'com.google.code.gson:gson:2.10.1'
|
||||
compileOnly 'com.google.code.gson:gson:2.14.0'
|
||||
|
||||
// Provided by Minecraft/Fabric runtime; do not shade it into the mod jar.
|
||||
compileOnly 'org.slf4j:slf4j-api:2.0.17'
|
||||
|
||||
bundledLibrary 'io.github.classgraph:classgraph:4.8.165'
|
||||
bundledLibrary 'io.github.classgraph:classgraph:4.8.192'
|
||||
|
||||
// Javet core only. Native V8 binaries are copied into resources/javet/natives.
|
||||
bundledLibrary "com.caoccao.javet:javet:${javetVersion}"
|
||||
|
|
@ -326,6 +329,11 @@ tasks.withType(JavaCompile).configureEach {
|
|||
}
|
||||
}
|
||||
|
||||
tasks.named("compileDevJava").configure { enabled = lovisualDevEnabled.get() }
|
||||
tasks.named("processDevResources").configure { enabled = lovisualDevEnabled.get() }
|
||||
tasks.named("compileVulkanDebugJava").configure { enabled = lovisualVulkanDebugEnabled.get() }
|
||||
tasks.named("processVulkanDebugResources").configure { enabled = lovisualVulkanDebugEnabled.get() }
|
||||
|
||||
java {
|
||||
def javaVersion = JavaVersion.toVersion(targetJavaVersion)
|
||||
if (JavaVersion.current() < javaVersion) {
|
||||
|
|
@ -337,6 +345,65 @@ java {
|
|||
withSourcesJar()
|
||||
}
|
||||
|
||||
pmd {
|
||||
toolVersion = '7.27.0'
|
||||
ruleSetFiles = files("config/pmd/ruleset.xml")
|
||||
ruleSets = []
|
||||
ignoreFailures = true
|
||||
consoleOutput = true
|
||||
sourceSets = [sourceSets.main]
|
||||
}
|
||||
|
||||
checkstyle {
|
||||
toolVersion = '14.1.0'
|
||||
configFile = file("config/checkstyle/checkstyle.xml")
|
||||
ignoreFailures = true
|
||||
showViolations = true
|
||||
sourceSets = [sourceSets.main]
|
||||
}
|
||||
|
||||
spotbugs {
|
||||
toolVersion = '4.10.3'
|
||||
ignoreFailures = true
|
||||
showProgress = true
|
||||
}
|
||||
|
||||
spotbugsMain {
|
||||
reports {
|
||||
html.required = true
|
||||
xml.required = false
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(com.github.spotbugs.snom.SpotBugsTask).configureEach {
|
||||
if (it.name != "spotbugsMain") {
|
||||
it.enabled = false
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("checkFolderLimit") {
|
||||
group = "verification"
|
||||
description = "Проверяет правило ≤4 файла на папку (проектное)"
|
||||
doLast {
|
||||
def base = file("src/main/java")
|
||||
def bad = []
|
||||
base.eachDirRecurse { dir ->
|
||||
def files = dir.listFiles({ f -> f.name.endsWith(".java") } as FileFilter)
|
||||
def count = files != null ? files.length : 0
|
||||
if (count > 4) bad << "${dir} : ${count}"
|
||||
}
|
||||
if (!bad.isEmpty()) {
|
||||
logger.warn("Папок >4 файлов:\n" + bad.join("\n"))
|
||||
if (!project.hasProperty("ignoreFolderLimit")) {
|
||||
throw new GradleException("Правило ≤4 файла/папку нарушено:\n" + bad.join("\n") + "\nИсправь разбивкой на подпапки")
|
||||
}
|
||||
} else {
|
||||
logger.lifecycle("checkFolderLimit: OK (0 папок >4)")
|
||||
}
|
||||
}
|
||||
}
|
||||
check.dependsOn("checkFolderLimit")
|
||||
|
||||
tasks.matching { it.name == "sourcesJar" }.configureEach {
|
||||
dependsOn tasks.named("generateSvgMsdf")
|
||||
exclude("javet/natives/**")
|
||||
|
|
|
|||
51
combatant-client-26.2/config/checkstyle/checkstyle.xml
Normal file
51
combatant-client-26.2/config/checkstyle/checkstyle.xml
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<?xml version="1.0"?>
|
||||
<!DOCTYPE module PUBLIC
|
||||
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
|
||||
"https://checkstyle.org/dtds/configuration_1_3.dtd">
|
||||
<module name="Checker">
|
||||
<property name="severity" value="warning"/>
|
||||
<property name="fileExtensions" value="java"/>
|
||||
|
||||
<module name="FileLength">
|
||||
<property name="max" value="200"/>
|
||||
<message key="maxLen.file" value="Файл {1} строк > 200 (правило проекта, фасад-исключение помечай @SuppressWarnings)"/>
|
||||
</module>
|
||||
|
||||
<module name="LineLength">
|
||||
<property name="max" value="140"/>
|
||||
<property name="ignorePattern" value="^(import|package).*"/>
|
||||
</module>
|
||||
|
||||
<module name="TreeWalker">
|
||||
<!-- Google base + проектные -->
|
||||
<module name="AvoidStarImport">
|
||||
<property name="severity" value="warning"/>
|
||||
</module>
|
||||
<module name="MagicNumber">
|
||||
<property name="ignoreNumbers" value="-1, 0, 1, 2"/>
|
||||
<property name="ignoreHashCodeMethod" value="true"/>
|
||||
<property name="ignoreAnnotation" value="true"/>
|
||||
</module>
|
||||
<module name="VisibilityModifier">
|
||||
<property name="packageAllowed" value="true"/>
|
||||
<property name="protectedAllowed" value="true"/>
|
||||
</module>
|
||||
<module name="MethodLength">
|
||||
<property name="max" value="40"/>
|
||||
</module>
|
||||
<module name="AvoidInlineConditionals"/>
|
||||
<module name="EmptyBlock">
|
||||
<property name="option" value="statement"/>
|
||||
</module>
|
||||
<module name="NeedBraces"/>
|
||||
<module name="LeftCurly"/>
|
||||
<module name="RightCurly"/>
|
||||
<module name="WhitespaceAround"/>
|
||||
<module name="NoWhitespaceBefore"/>
|
||||
<module name="GenericWhitespace"/>
|
||||
<module name="IllegalImport">
|
||||
<property name="illegalPkgs" value="java.awt, javax.swing"/>
|
||||
</module>
|
||||
<!-- ≤4 файла на папку Checkstyle не умеет — проверяется скриптом tools/check_folders.sh -->
|
||||
</module>
|
||||
</module>
|
||||
63
combatant-client-26.2/config/pmd/ruleset.xml
Normal file
63
combatant-client-26.2/config/pmd/ruleset.xml
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
<?xml version="1.0"?>
|
||||
<ruleset name="LoVisual"
|
||||
xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd">
|
||||
|
||||
<description>LoVisual — bestpractices/errorprone/performance/security + проектные правила 200/4</description>
|
||||
|
||||
<!-- Базовые категории PMD 7.27.0 -->
|
||||
<rule ref="category/java/bestpractices.xml"/>
|
||||
<rule ref="category/java/errorprone.xml"/>
|
||||
<rule ref="category/java/performance.xml"/>
|
||||
<rule ref="category/java/security.xml"/>
|
||||
|
||||
<!-- Файл ≤200 строк: через ExcessiveClassLength (класс ≈ файл) -->
|
||||
<rule ref="category/java/design.xml/ExcessiveClassLength">
|
||||
<properties>
|
||||
<property name="minimum" value="200"/>
|
||||
</properties>
|
||||
</rule>
|
||||
|
||||
<!-- Кастом: System.currentTimeMillis → GameClock.millis() -->
|
||||
<rule name="SystemCurrentTimeMillisUsage"
|
||||
language="java"
|
||||
message="Используй GameClock.millis() вместо System.currentTimeMillis() в модулях"
|
||||
class="net.sourceforge.pmd.lang.rule.XPathRule">
|
||||
<description>Модули должны использовать игровое время GameClock.millis(), не стенное.</description>
|
||||
<priority>2</priority>
|
||||
<properties>
|
||||
<property name="xpath">
|
||||
<value><![CDATA[
|
||||
//PrimaryExpression[PrimaryPrefix/Name[@Image='System.currentTimeMillis']]
|
||||
]]></value>
|
||||
</property>
|
||||
</properties>
|
||||
</rule>
|
||||
|
||||
<!-- Кастом: static Minecraft в полях -->
|
||||
<rule name="StaticMinecraftField"
|
||||
language="java"
|
||||
message="Не храни static Minecraft в поле — только локально Minecraft.getInstance()"
|
||||
class="net.sourceforge.pmd.lang.rule.XPathRule">
|
||||
<description>Никакого static final Minecraft в полях (правило проекта).</description>
|
||||
<priority>2</priority>
|
||||
<properties>
|
||||
<property name="xpath">
|
||||
<value><![CDATA[
|
||||
//FieldDeclaration[@Static='true'][.//Type//Name[@Image='Minecraft']]
|
||||
]]></value>
|
||||
</property>
|
||||
</properties>
|
||||
</rule>
|
||||
|
||||
<!-- Пустые блоки / пустышки -->
|
||||
<rule ref="category/java/codestyle.xml/EmptyCatchBlock"/>
|
||||
<rule ref="category/java/codestyle.xml/EmptyIfStmt"/>
|
||||
<rule ref="category/java/bestpractices.xml/UnusedPrivateMethod"/>
|
||||
<rule ref="category/java/bestpractices.xml/UnusedPrivateField"/>
|
||||
|
||||
<!-- Исключения: сгенерированные, миксины, Faust -->
|
||||
<exclude-pattern>.*/build/.*</exclude-pattern>
|
||||
<exclude-pattern>.*/src/main/resources/.*</exclude-pattern>
|
||||
</ruleset>
|
||||
|
|
@ -3,7 +3,7 @@ org.gradle.jvmargs=-Xmx2G
|
|||
# Fabric Properties
|
||||
# check these on https://modmuss50.me/fabric.html
|
||||
minecraft_version=26.2
|
||||
loader_version=0.19.3
|
||||
loader_version=0.19.4
|
||||
# Mod Properties
|
||||
mod_version=0.1.1
|
||||
maven_group=dev.loki
|
||||
|
|
|
|||
|
|
@ -66,7 +66,6 @@ import dev.loki.lovisual.util.network.control.BacktrackController;
|
|||
import dev.loki.lovisual.util.network.control.BlinkManager;
|
||||
import dev.loki.lovisual.util.proxy.ProxyBackend;
|
||||
import dev.loki.lovisual.util.network.control.FakeLagController;
|
||||
import dev.loki.lovisual.util.player.inventory.core.InventorySwap;
|
||||
import dev.loki.lovisual.util.player.simulation.PlayerSimulationCache;
|
||||
import dev.loki.lovisual.util.pvp.tracker.PvpTargetTracker;
|
||||
import dev.loki.lovisual.util.pvp.tracker.PvpTracker;
|
||||
|
|
@ -316,7 +315,6 @@ public class LoVisual implements ClientModInitializer {
|
|||
Events.BUS.register(RenderPrewarmContributors.INSTANCE);
|
||||
Events.BUS.register(RotationManager.INSTANCE);
|
||||
Events.BUS.register(AttackPressing.INSTANCE);
|
||||
Events.BUS.register(InventorySwap.INSTANCE);
|
||||
Events.BUS.register(SprintController.INSTANCE);
|
||||
Events.BUS.register(AntiBotTracker.INSTANCE);
|
||||
Events.BUS.register(CombatProtocolHeuristics.INSTANCE);
|
||||
|
|
|
|||
|
|
@ -108,10 +108,6 @@ public enum CommonSettingSchemas {
|
|||
public static final CommonSettingSchema COMBAT_ON_ITEM_USE = key("combat.on_item_use");
|
||||
public static final CommonSettingSchema COMBAT_CRYSTAL_DISTANCE = key("combat.crystal_distance");
|
||||
public static final CommonSettingSchema COMBAT_BREAK_SHIELDS = key("combat.break_shields");
|
||||
public static final CommonSettingSchema INVENTORY_SEARCH_SCOPE = key("inventory.search_scope", "swap_scope");
|
||||
public static final CommonSettingSchema INVENTORY_SWAP_VISIBILITY = key("inventory.swap_visibility", "swap_visibility");
|
||||
public static final CommonSettingSchema INVENTORY_RESTORE_ITEM = key("inventory.restore_item", "restore_item");
|
||||
public static final CommonSettingSchema INVENTORY_PREFER_HOTBAR = key("inventory.prefer_hotbar", "prefer_hotbar");
|
||||
public static final CommonSettingSchema INTERACTION_IGNORE_OPEN_INVENTORY = key("interaction.ignore_open_inventory", "ignore_open_inventory");
|
||||
public static final CommonSettingSchema COMBAT_ONLY_PLAYERS = key("combat.only_players");
|
||||
public static final CommonSettingSchema COMBAT_ONLY_BLOCKING = key("combat.only_blocking");
|
||||
|
|
@ -135,7 +131,7 @@ public enum CommonSettingSchemas {
|
|||
public static final CommonSettingSchema PLACEMENT_MOVEMENT_CORRECTION = key("placement.movement_correction", "rotation.movement_correction", "movement_correction");
|
||||
public static final CommonSettingSchema PLACEMENT_ROTATION_RESET_TICKS = key("placement.rotation_reset_ticks", "rotation.rotation_reset_ticks", "rotation_reset_ticks");
|
||||
public static final CommonSettingSchema PLACEMENT_ROTATION_RESET_THRESHOLD = key("placement.rotation_reset_threshold", "rotation.rotation_reset_threshold", "rotation_reset_threshold");
|
||||
public static final CommonSettingSchema INVENTORY_SWAP_MODE = key("inventory.swap_mode", "swap_mode");
|
||||
|
||||
public static final CommonSettingSchema PLAYER_HEALTH_THRESHOLD = key("player.health_threshold", "pause_health", "health_threshold");
|
||||
public static final CommonSettingSchema PLAYER_ELYTRA_HEALTH = key("player.elytra_health");
|
||||
public static final CommonSettingSchema PLAYER_FALL_CHECK = key("player.fall_check");
|
||||
|
|
|
|||
|
|
@ -16,10 +16,6 @@ import dev.loki.lovisual.config.values.primitive.NumberValue;
|
|||
import dev.loki.lovisual.render.iris.runtime.IrisRuntime;
|
||||
import dev.loki.lovisual.util.logging.DebugLog;
|
||||
import dev.loki.lovisual.util.logging.DebugMode;
|
||||
import dev.loki.lovisual.util.player.inventory.support.InventorySearchScope;
|
||||
import dev.loki.lovisual.util.player.inventory.core.InventorySwap;
|
||||
import dev.loki.lovisual.util.player.inventory.core.InventorySwapPolicy;
|
||||
import dev.loki.lovisual.util.player.inventory.support.InventorySwapVisibility;
|
||||
import net.minecraft.client.resources.language.I18n;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
|
@ -66,38 +62,6 @@ public final class MainConfig implements ConfigObject, ConfigNameProvider, Setti
|
|||
private final BooleanValue clickGuiHudEditorHints =
|
||||
new BooleanValue("clickGuiHudEditorHints", true);
|
||||
|
||||
//@CFGComment("Default inventory swap safety policy: none / grim_strict / legit")
|
||||
private final EnumValue<InventorySwapPolicy> inventorySwapPolicy =
|
||||
new EnumValue<>("inventorySwapPolicy", InventorySwapPolicy.NONE, InventorySwapPolicy.class);
|
||||
|
||||
//@CFGComment("Default inventory swap search scope: hotbar / inventory / full")
|
||||
private final EnumValue<InventorySearchScope> inventorySwapScope =
|
||||
new EnumValue<>("inventorySwapScope", InventorySearchScope.FULL, InventorySearchScope.class);
|
||||
|
||||
//@CFGComment("Default inventory swap visibility: normal / silent")
|
||||
private final EnumValue<InventorySwapVisibility> inventorySwapVisibility =
|
||||
new EnumValue<>("inventorySwapVisibility", InventorySwapVisibility.SILENT, InventorySwapVisibility.class);
|
||||
|
||||
//@CFGComment("Restore the original hotbar slot after default swap requests")
|
||||
private final BooleanValue inventorySwapRestore =
|
||||
new BooleanValue("inventorySwapRestore", true);
|
||||
|
||||
//@CFGComment("Prefer hotbar matches before inventory matches for full-scope swap requests")
|
||||
private final BooleanValue inventorySwapPreferHotbar =
|
||||
new BooleanValue("inventorySwapPreferHotbar", true);
|
||||
|
||||
//@CFGComment("Extra delay before queued legit inventory swap actions")
|
||||
private final NumberValue<Integer> inventorySwapLegitWaitTicks =
|
||||
new NumberValue<>("inventorySwapLegitWaitTicks", 2, 0, 20);
|
||||
|
||||
// @CFGComment("Extra delay before queued Grim-strict inventory click/close actions")
|
||||
private final NumberValue<Integer> inventorySwapStrictInventoryWaitTicks =
|
||||
new NumberValue<>("inventorySwapStrictInventoryWaitTicks", 1, 0, 20);
|
||||
|
||||
//@CFGComment("Movement input lock duration while waiting for Grim-strict inventory actions")
|
||||
private final NumberValue<Integer> inventorySwapStrictMovementLockTicks =
|
||||
new NumberValue<>("inventorySwapStrictMovementLockTicks", 2, 0, 20);
|
||||
|
||||
//@CFGComment("Hard-disable narrator and narrator hotkey")
|
||||
private final BooleanValue disableNarrator =
|
||||
new BooleanValue("disableNarrator", true);
|
||||
|
|
@ -137,7 +101,6 @@ public final class MainConfig implements ConfigObject, ConfigNameProvider, Setti
|
|||
private MainConfig() {
|
||||
ConfigSerializer.load(this);
|
||||
applyDebugMode();
|
||||
applyInventorySwapSettings();
|
||||
}
|
||||
|
||||
public static MainConfig get() {
|
||||
|
|
@ -165,7 +128,6 @@ public final class MainConfig implements ConfigObject, ConfigNameProvider, Setti
|
|||
@Override
|
||||
public void saveConfig() {
|
||||
applyDebugMode();
|
||||
applyInventorySwapSettings();
|
||||
ConfigSerializer.requestSave(this);
|
||||
}
|
||||
|
||||
|
|
@ -235,110 +197,6 @@ public final class MainConfig implements ConfigObject, ConfigNameProvider, Setti
|
|||
ConfigSerializer.requestSave(this);
|
||||
}
|
||||
|
||||
public InventorySwapPolicy getInventorySwapPolicy() {
|
||||
return inventorySwapPolicy.get();
|
||||
}
|
||||
|
||||
public void setInventorySwapPolicy(InventorySwapPolicy policy) {
|
||||
inventorySwapPolicy.set(policy != null ? policy : InventorySwapPolicy.NONE);
|
||||
saveAndApplyInventorySwapSettings();
|
||||
}
|
||||
|
||||
public InventorySearchScope getInventorySwapScope() {
|
||||
return inventorySwapScope.get();
|
||||
}
|
||||
|
||||
public void setInventorySwapScope(InventorySearchScope scope) {
|
||||
inventorySwapScope.set(scope != null ? scope : InventorySearchScope.FULL);
|
||||
saveAndApplyInventorySwapSettings();
|
||||
}
|
||||
|
||||
public InventorySwapVisibility getInventorySwapVisibility() {
|
||||
return inventorySwapVisibility.get();
|
||||
}
|
||||
|
||||
public void setInventorySwapVisibility(InventorySwapVisibility visibility) {
|
||||
inventorySwapVisibility.set(visibility != null ? visibility : InventorySwapVisibility.SILENT);
|
||||
saveAndApplyInventorySwapSettings();
|
||||
}
|
||||
|
||||
public boolean isInventorySwapRestore() {
|
||||
return inventorySwapRestore.get();
|
||||
}
|
||||
|
||||
public void setInventorySwapRestore(boolean restore) {
|
||||
inventorySwapRestore.set(restore);
|
||||
saveAndApplyInventorySwapSettings();
|
||||
}
|
||||
|
||||
public boolean isInventorySwapPreferHotbar() {
|
||||
return inventorySwapPreferHotbar.get();
|
||||
}
|
||||
|
||||
public void setInventorySwapPreferHotbar(boolean preferHotbar) {
|
||||
inventorySwapPreferHotbar.set(preferHotbar);
|
||||
saveAndApplyInventorySwapSettings();
|
||||
}
|
||||
|
||||
public int getInventorySwapLegitWaitTicks() {
|
||||
return inventorySwapLegitWaitTicks.get();
|
||||
}
|
||||
|
||||
public void setInventorySwapLegitWaitTicks(int ticks) {
|
||||
inventorySwapLegitWaitTicks.fromJson(ticks);
|
||||
saveAndApplyInventorySwapSettings();
|
||||
}
|
||||
|
||||
public int getInventorySwapStrictInventoryWaitTicks() {
|
||||
return inventorySwapStrictInventoryWaitTicks.get();
|
||||
}
|
||||
|
||||
public void setInventorySwapStrictInventoryWaitTicks(int ticks) {
|
||||
inventorySwapStrictInventoryWaitTicks.fromJson(ticks);
|
||||
saveAndApplyInventorySwapSettings();
|
||||
}
|
||||
|
||||
public int getInventorySwapStrictMovementLockTicks() {
|
||||
return inventorySwapStrictMovementLockTicks.get();
|
||||
}
|
||||
|
||||
public void setInventorySwapStrictMovementLockTicks(int ticks) {
|
||||
inventorySwapStrictMovementLockTicks.fromJson(ticks);
|
||||
saveAndApplyInventorySwapSettings();
|
||||
}
|
||||
|
||||
public EnumValue<InventorySwapPolicy> inventorySwapPolicyValue() {
|
||||
return inventorySwapPolicy;
|
||||
}
|
||||
|
||||
public EnumValue<InventorySearchScope> inventorySwapScopeValue() {
|
||||
return inventorySwapScope;
|
||||
}
|
||||
|
||||
public EnumValue<InventorySwapVisibility> inventorySwapVisibilityValue() {
|
||||
return inventorySwapVisibility;
|
||||
}
|
||||
|
||||
public BooleanValue inventorySwapRestoreValue() {
|
||||
return inventorySwapRestore;
|
||||
}
|
||||
|
||||
public BooleanValue inventorySwapPreferHotbarValue() {
|
||||
return inventorySwapPreferHotbar;
|
||||
}
|
||||
|
||||
public NumberValue<Integer> inventorySwapLegitWaitTicksValue() {
|
||||
return inventorySwapLegitWaitTicks;
|
||||
}
|
||||
|
||||
public NumberValue<Integer> inventorySwapStrictInventoryWaitTicksValue() {
|
||||
return inventorySwapStrictInventoryWaitTicks;
|
||||
}
|
||||
|
||||
public NumberValue<Integer> inventorySwapStrictMovementLockTicksValue() {
|
||||
return inventorySwapStrictMovementLockTicks;
|
||||
}
|
||||
|
||||
public List<SettingDef> getImageSettingDefs() {
|
||||
List<SettingDef> list = new ArrayList<>();
|
||||
list.add(SettingDef.mode(msaa3d)
|
||||
|
|
@ -377,16 +235,7 @@ public final class MainConfig implements ConfigObject, ConfigNameProvider, Setti
|
|||
}
|
||||
|
||||
public List<SettingDef> getUtilitySettingDefs() {
|
||||
List<SettingDef> list = new ArrayList<>();
|
||||
list.add(SettingDef.mode(inventorySwapPolicy).common("inventory.swap_policy"));
|
||||
list.add(SettingDef.mode(inventorySwapScope).common("inventory.search_scope"));
|
||||
list.add(SettingDef.mode(inventorySwapVisibility).common("inventory.swap_visibility"));
|
||||
list.add(SettingDef.bool(inventorySwapRestore).common("inventory.restore_item"));
|
||||
list.add(SettingDef.bool(inventorySwapPreferHotbar).common("inventory.prefer_hotbar"));
|
||||
list.add(SettingDef.number(inventorySwapLegitWaitTicks).common("inventory.legit_wait_ticks"));
|
||||
list.add(SettingDef.number(inventorySwapStrictInventoryWaitTicks).common("inventory.strict_inventory_wait_ticks"));
|
||||
list.add(SettingDef.number(inventorySwapStrictMovementLockTicks).common("inventory.strict_movement_lock_ticks"));
|
||||
return list;
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
public boolean isNarratorDisabled() {
|
||||
|
|
@ -437,14 +286,6 @@ public final class MainConfig implements ConfigObject, ConfigNameProvider, Setti
|
|||
list.add(msaa3d);
|
||||
list.add(clickGuiModulesHints);
|
||||
list.add(clickGuiHudEditorHints);
|
||||
list.add(inventorySwapPolicy);
|
||||
list.add(inventorySwapScope);
|
||||
list.add(inventorySwapVisibility);
|
||||
list.add(inventorySwapRestore);
|
||||
list.add(inventorySwapPreferHotbar);
|
||||
list.add(inventorySwapLegitWaitTicks);
|
||||
list.add(inventorySwapStrictInventoryWaitTicks);
|
||||
list.add(inventorySwapStrictMovementLockTicks);
|
||||
list.add(disableNarrator);
|
||||
list.add(backdoorProtection);
|
||||
list.add(backdoorTranslationFilter);
|
||||
|
|
@ -457,20 +298,4 @@ public final class MainConfig implements ConfigObject, ConfigNameProvider, Setti
|
|||
return list;
|
||||
}
|
||||
|
||||
private void saveAndApplyInventorySwapSettings() {
|
||||
applyInventorySwapSettings();
|
||||
ConfigSerializer.requestSave(this);
|
||||
}
|
||||
|
||||
private void applyInventorySwapSettings() {
|
||||
InventorySwap swap = InventorySwap.INSTANCE;
|
||||
swap.setDefaultPolicy(inventorySwapPolicy.get());
|
||||
swap.setDefaultScope(inventorySwapScope.get());
|
||||
swap.setDefaultVisibility(inventorySwapVisibility.get());
|
||||
swap.setRestoreByDefault(inventorySwapRestore.get());
|
||||
swap.setPreferHotbar(inventorySwapPreferHotbar.get());
|
||||
swap.setLegitWaitTicks(inventorySwapLegitWaitTicks.get());
|
||||
swap.setStrictInventoryWaitTicks(inventorySwapStrictInventoryWaitTicks.get());
|
||||
swap.setStrictMovementLockTicks(inventorySwapStrictMovementLockTicks.get());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.events.impl.render;
|
||||
package dev.loki.lovisual.events.impl.render.particles;
|
||||
|
||||
import net.minecraft.core.particles.ParticleOptions;
|
||||
import dev.loki.lovisual.events.Event;
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.command.impl.world;
|
||||
package dev.loki.lovisual.features.command.impl.world.integration;
|
||||
|
||||
import dev.loki.lovisual.features.command.util.ClientCommand;
|
||||
import dev.loki.lovisual.features.command.core.CommandContext;
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.command.impl.world;
|
||||
package dev.loki.lovisual.features.command.impl.world.integration;
|
||||
|
||||
import dev.loki.lovisual.compat.xaero.XaeroWaypointStore;
|
||||
import dev.loki.lovisual.features.command.util.ClientCommand;
|
||||
|
|
@ -12,7 +12,7 @@ import dev.loki.lovisual.render.engine.text.render.TextRenderer;
|
|||
import dev.loki.lovisual.render.helpers.clip.ClipFunction;
|
||||
import net.minecraft.util.Mth;
|
||||
import java.util.List;
|
||||
import static dev.loki.lovisual.render.engine.text.glyph.TextGlyphFallback.isSvgFontKey;
|
||||
import static dev.loki.lovisual.render.engine.text.glyph.custom.TextGlyphFallback.isSvgFontKey;
|
||||
import static dev.loki.lovisual.features.theme.Theme.theme;
|
||||
import static dev.loki.lovisual.features.gui.chat.text.util.ChatHoverUtil.HoverTip;
|
||||
import dev.loki.lovisual.features.gui.chat.menu.BetterChatTooltipRenderer;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ package dev.loki.lovisual.features.gui.chat.text;
|
|||
|
||||
import dev.loki.lovisual.features.command.util.CommandOutput;
|
||||
import dev.loki.lovisual.render.engine.svg.model.SvgRenderOptions;
|
||||
import dev.loki.lovisual.render.engine.text.glyph.TextGlyphFallback;
|
||||
import dev.loki.lovisual.render.engine.text.glyph.custom.TextGlyphFallback;
|
||||
import dev.loki.lovisual.render.engine.text.render.TextRenderer;
|
||||
import dev.loki.lovisual.util.time.GameClock;
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import net.minecraft.network.chat.Style;
|
|||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import dev.loki.lovisual.render.engine.text.glyph.TextGlyphFallback;
|
||||
import dev.loki.lovisual.render.engine.text.glyph.custom.TextGlyphFallback;
|
||||
import dev.loki.lovisual.render.engine.text.render.TextRenderer;
|
||||
|
||||
import static dev.loki.lovisual.features.theme.Theme.theme;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ package dev.loki.lovisual.features.gui.chat.text.util;
|
|||
|
||||
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.TextGlyphFallback;
|
||||
import dev.loki.lovisual.render.engine.text.glyph.custom.TextGlyphFallback;
|
||||
import dev.loki.lovisual.render.engine.text.render.TextRenderer;
|
||||
import dev.loki.lovisual.render.engine.text.glyph.VanillaTextRenderer;
|
||||
import net.minecraft.network.chat.Style;
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.Lay
|
|||
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.ClickGuiHintOverlay;
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.
|
|||
import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.theme.ThemeComponent;
|
||||
import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.implement.theme.ThemeEditorPreviewComponent;
|
||||
import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.LayoutRender2D;
|
||||
import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiHintOverlay;
|
||||
import dev.loki.lovisual.features.gui.clickgui.util.tooltip.ClickGuiHintOverlay;
|
||||
import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiI18n;
|
||||
import dev.loki.lovisual.features.theme.EditableClickGuiTheme;
|
||||
import dev.loki.lovisual.render.engine.animation.AnimationUtility;
|
||||
|
|
|
|||
|
|
@ -18,9 +18,9 @@ import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.Lay
|
|||
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.ClickGuiHintOverlay;
|
||||
import dev.loki.lovisual.features.gui.clickgui.util.tooltip.ClickGuiHintOverlay;
|
||||
import dev.loki.lovisual.features.gui.clickgui.util.ClickGuiI18n;
|
||||
import dev.loki.lovisual.features.gui.clickgui.util.SettingTooltip;
|
||||
import dev.loki.lovisual.features.gui.clickgui.util.tooltip.SettingTooltip;
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ 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.TextGlyphFallback;
|
||||
import dev.loki.lovisual.render.engine.text.glyph.custom.TextGlyphFallback;
|
||||
import dev.loki.lovisual.render.engine.text.render.TextRenderer;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.Style;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.gui.clickgui.util;
|
||||
package dev.loki.lovisual.features.gui.clickgui.util.tooltip;
|
||||
|
||||
import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer;
|
||||
import dev.loki.lovisual.render.engine.animation.AnimationUtility;
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.gui.clickgui.util;
|
||||
package dev.loki.lovisual.features.gui.clickgui.util.tooltip;
|
||||
|
||||
import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette;
|
||||
import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.LayoutRender2D;
|
||||
|
|
@ -43,7 +43,7 @@ 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.TextGlyphFallback;
|
||||
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;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* 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.buttons.render;
|
||||
|
||||
import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil;
|
||||
import dev.loki.lovisual.features.gui.hud.nondraggable.impl.buttons.state.ButtonHoverState;
|
||||
import dev.loki.lovisual.render.engine.core.context.ViewportContext;
|
||||
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.gui.components.AbstractWidget;
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
import static dev.loki.lovisual.features.theme.Theme.theme;
|
||||
|
||||
/**
|
||||
* Рендер слайдера: фон через ButtonStyleRenderer + трек/заливка.
|
||||
*/
|
||||
public final class ButtonSliderRenderer {
|
||||
private ButtonSliderRenderer() {
|
||||
}
|
||||
|
||||
public static void render(GuiGraphicsExtractor ctx, AbstractWidget widget, float value, boolean dragging, float hover) {
|
||||
if (ctx == null || widget == null) return;
|
||||
|
||||
float x = widget.getX();
|
||||
float y = widget.getY();
|
||||
float w = widget.getWidth();
|
||||
float h = widget.getHeight();
|
||||
float scale = ViewportContext.getScaleFactor();
|
||||
|
||||
float alpha = widget.getAlpha();
|
||||
float radius = Math.min(4.0f, h * 0.5f);
|
||||
ButtonSprites.markRounded(widget, radius);
|
||||
|
||||
ButtonStyleRenderer.renderInternal(ctx, widget, null, hover, widget.isActive(), widget.isFocused(), scale, true, false);
|
||||
|
||||
float trackInset = 8f;
|
||||
float trackX = x + trackInset;
|
||||
float trackW = Math.max(12f, w - trackInset * 2f);
|
||||
float trackH = Math.max(4f, h * 0.24f);
|
||||
float trackY = y + (h - trackH) * 0.5f;
|
||||
float fillW = trackW * value;
|
||||
|
||||
float hoverEase = ButtonHoverState.smoothHover(hover);
|
||||
int track = HudRenderUtil.mixColor(ButtonSprites.forceOpaque(theme().surface()), 0xFF000000, 0.65f);
|
||||
int fill = HudRenderUtil.mixColor(ButtonSprites.forceOpaque(theme().accentSoft()), ButtonSprites.forceOpaque(theme().accent()), 0.25f + 0.35f * hoverEase);
|
||||
int stroke = HudRenderUtil.mixColor(ButtonSprites.forceOpaque(theme().strokeSoft()), 0xFF000000, 0.55f);
|
||||
stroke = HudRenderUtil.mixColor(stroke, theme().accent(), 0.08f * hoverEase);
|
||||
|
||||
track = HudRenderUtil.scaleAlpha(track, alpha);
|
||||
fill = HudRenderUtil.scaleAlpha(fill, alpha);
|
||||
stroke = HudRenderUtil.scaleAlpha(stroke, 0.6f * alpha);
|
||||
|
||||
float trackXb = trackX * scale;
|
||||
float trackYb = trackY * scale;
|
||||
float trackWb = trackW * scale;
|
||||
float trackHb = trackH * scale;
|
||||
float fillWb = fillW * scale;
|
||||
|
||||
ViewportContext.beginUnscaled(null);
|
||||
boolean batching = Renderer2D.isBatching();
|
||||
if (!batching) {
|
||||
Renderer2D.COLOR.begin();
|
||||
}
|
||||
float barRadius = Math.min(2.5f * scale, trackHb * 0.5f);
|
||||
Renderer2D.COLOR.roundedRect(trackXb, trackYb, trackWb, trackHb, barRadius, scale, track);
|
||||
if (fillW > 1f) {
|
||||
Renderer2D.COLOR.roundedRect(trackXb, trackYb, fillWb, trackHb, barRadius, scale, fill);
|
||||
}
|
||||
Renderer2D.COLOR.roundedRectStroke(trackXb, trackYb, trackWb, trackHb, barRadius, scale, scale, stroke);
|
||||
if (!batching) {
|
||||
Renderer2D.COLOR.render();
|
||||
}
|
||||
ViewportContext.end(null);
|
||||
|
||||
Component text = widget.getMessage();
|
||||
ButtonTextRenderer.renderCentered(text, x, y, w, h, alpha, widget.isActive(), hover);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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.buttons.render;
|
||||
|
||||
import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil;
|
||||
import dev.loki.lovisual.mixininterface.entity.IRoundedHitbox;
|
||||
import dev.loki.lovisual.render.engine.core.context.ViewportContext;
|
||||
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.components.AbstractWidget;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlas;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
|
||||
import net.minecraft.data.AtlasIds;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/**
|
||||
* Геометрия/спрайты кнопок: скруглённый hitbox, отрисовка GUI-спрайтов.
|
||||
*/
|
||||
public final class ButtonSprites {
|
||||
private ButtonSprites() {
|
||||
}
|
||||
|
||||
public static void markRounded(AbstractWidget widget, float radius) {
|
||||
if (widget instanceof IRoundedHitbox rounded) {
|
||||
rounded.lovisual$setRoundedHitbox(radius);
|
||||
}
|
||||
}
|
||||
|
||||
public static int forceOpaque(int argb) {
|
||||
return (argb & 0x00FFFFFF) | 0xFF000000;
|
||||
}
|
||||
|
||||
public static void drawGuiSprite(Identifier spriteId, int x, int y, int w, int h, float alpha) {
|
||||
if (spriteId == null) return;
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
if (mc == null) return;
|
||||
TextureAtlas atlas = mc.getAtlasManager().getAtlasOrThrow(AtlasIds.GUI);
|
||||
if (atlas == null) return;
|
||||
TextureAtlasSprite sprite = atlas.getSprite(spriteId);
|
||||
if (sprite == null) return;
|
||||
Identifier atlasId = atlas.location();
|
||||
if (atlasId == null) return;
|
||||
|
||||
float scale = ViewportContext.getScaleFactor();
|
||||
float xb = x * scale;
|
||||
float yb = y * scale;
|
||||
float wb = w * scale;
|
||||
float hb = h * scale;
|
||||
int argb = HudRenderUtil.scaleAlpha(0xFFFFFFFF, alpha);
|
||||
|
||||
ViewportContext.beginUnscaled(null);
|
||||
Renderer2D.TEXTURE.begin();
|
||||
Renderer2D.TEXTURE.texQuad(xb, yb, wb, hb, sprite.getU0(), sprite.getV0(), sprite.getU1(), sprite.getV1(), argb);
|
||||
Renderer2D.TEXTURE.end();
|
||||
Renderer2D.TEXTURE.render(atlasId);
|
||||
ViewportContext.end(null);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
/*
|
||||
* 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.buttons.render;
|
||||
import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil;
|
||||
import dev.loki.lovisual.features.gui.hud.nondraggable.impl.buttons.BetterButtons;
|
||||
import dev.loki.lovisual.features.gui.hud.nondraggable.impl.buttons.state.ButtonHoverState;
|
||||
import dev.loki.lovisual.features.gui.hud.nondraggable.impl.buttons.state.ButtonParallax;
|
||||
import dev.loki.lovisual.render.engine.core.context.ViewportContext;
|
||||
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
|
||||
import dev.loki.lovisual.render.engine.renderer.warp.RenderWarpStack;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.gui.components.AbstractWidget;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import static dev.loki.lovisual.features.theme.Theme.theme;
|
||||
/**
|
||||
* Стилизованный рендер кнопки: DEFAULT / GRADIENT / CUSTOM.
|
||||
*/
|
||||
public final class ButtonStyleRenderer {
|
||||
private static final float BUTTON_RADIUS = 6.0f;
|
||||
private static final float BUTTON_SOFTNESS = 1.2f;
|
||||
private ButtonStyleRenderer() {
|
||||
}
|
||||
public static void renderButton(GuiGraphicsExtractor ctx, AbstractWidget widget, Component text, float hover, boolean enabled, boolean focused) {
|
||||
renderInternal(ctx, widget, text, hover, enabled, focused, ViewportContext.getScaleFactor(), true, true);
|
||||
}
|
||||
public static void renderButtonUnscaled(GuiGraphicsExtractor ctx, AbstractWidget widget, Component text, float hover, boolean enabled, boolean focused) {
|
||||
renderInternal(ctx, widget, text, hover, enabled, focused, 1.0f, false, true);
|
||||
}
|
||||
public static void renderInternal(GuiGraphicsExtractor ctx, AbstractWidget widget, Component text, float hover, boolean enabled, boolean focused, float scale, boolean manageProjection, boolean allowParallax) {
|
||||
if (ctx == null || widget == null) return;
|
||||
float x = widget.getX();
|
||||
float y = widget.getY();
|
||||
float w = widget.getWidth();
|
||||
float h = widget.getHeight();
|
||||
BetterButtons cfg = BetterButtons.get();
|
||||
BetterButtons.Style mode = cfg.getStyle().get();
|
||||
float radiusBase = BUTTON_RADIUS;
|
||||
float softness = 0.0f;
|
||||
float strokeSoft = 0.0f;
|
||||
if (mode == BetterButtons.Style.GRADIENT) {
|
||||
softness = BUTTON_SOFTNESS;
|
||||
strokeSoft = BUTTON_SOFTNESS;
|
||||
} else if (mode == BetterButtons.Style.CUSTOM) {
|
||||
radiusBase = cfg.getCustomRadius().get();
|
||||
softness = cfg.getCustomSoftness().get();
|
||||
strokeSoft = cfg.getCustomSoftness().get();
|
||||
}
|
||||
float radius = Math.min(radiusBase, h * 0.5f);
|
||||
ButtonSprites.markRounded(widget, radius);
|
||||
float alpha = widget.getAlpha();
|
||||
float hoverEase = ButtonHoverState.smoothHover(hover);
|
||||
float xb = x * scale;
|
||||
float yb = y * scale;
|
||||
float wb = w * scale;
|
||||
float hb = h * scale;
|
||||
float rb = radius * scale;
|
||||
float softnessPx = softness * scale;
|
||||
float strokeSoftPx = strokeSoft * scale;
|
||||
ButtonParallax.Data parallax = allowParallax ? ButtonParallax.compute(widget, hover) : ButtonParallax.Data.NONE;
|
||||
if (manageProjection) {
|
||||
ViewportContext.beginUnscaled(null);
|
||||
}
|
||||
boolean batching = Renderer2D.isBatching();
|
||||
if (!batching) {
|
||||
Renderer2D.COLOR.begin();
|
||||
}
|
||||
try (RenderWarpStack.Scope ignored = ButtonParallax.pushWarp(parallax, xb, yb, wb, hb)) {
|
||||
if (mode == BetterButtons.Style.DEFAULT) {
|
||||
renderDefault(xb, yb, wb, hb, rb, alpha, hoverEase, enabled, focused, scale);
|
||||
} else if (mode == BetterButtons.Style.GRADIENT) {
|
||||
renderGradient(cfg, xb, yb, wb, hb, rb, softnessPx, strokeSoftPx, alpha, hoverEase, enabled, focused, scale);
|
||||
} else {
|
||||
renderCustom(cfg, xb, yb, wb, hb, rb, softnessPx, strokeSoftPx, alpha, hoverEase, enabled, focused, scale);
|
||||
}
|
||||
}
|
||||
if (!batching) {
|
||||
Renderer2D.COLOR.render();
|
||||
}
|
||||
if (manageProjection) {
|
||||
ViewportContext.end(null);
|
||||
}
|
||||
if (text != null && !text.getString().isEmpty()) {
|
||||
try (RenderWarpStack.Scope ignored = ButtonParallax.pushWarp(parallax, x, y, w, h)) {
|
||||
ButtonTextRenderer.renderCentered(text, x, y, w, h, alpha, enabled, hover, !parallax.active());
|
||||
}
|
||||
}
|
||||
}
|
||||
private static void renderDefault(float xb, float yb, float wb, float hb, float rb, float alpha, float hoverEase, boolean enabled, boolean focused, float scale) {
|
||||
int baseBg = enabled ? 0xFF1B1B1B : 0xFF141414;
|
||||
int hoverBg = enabled ? 0xFF2A2A2A : 0xFF141414;
|
||||
int accent = theme().accent();
|
||||
int darkBase = HudRenderUtil.mixColor(baseBg, 0xFF000000, 0.45f);
|
||||
float hoverMix = enabled ? (0.08f + 0.22f * hoverEase) : 0.0f;
|
||||
int bgBase = HudRenderUtil.mixColor(darkBase, hoverBg, hoverMix);
|
||||
bgBase = HudRenderUtil.mixColor(bgBase, accent, 0.18f * hoverEase);
|
||||
int strokeBase = HudRenderUtil.mixColor(bgBase, 0xFFFFFFFF, 0.18f);
|
||||
strokeBase = HudRenderUtil.mixColor(strokeBase, accent, 0.16f * hoverEase + (focused ? 0.06f : 0.0f));
|
||||
int bg = HudRenderUtil.scaleAlpha(bgBase, alpha);
|
||||
int stroke = HudRenderUtil.scaleAlpha(strokeBase, 0.82f * alpha);
|
||||
Renderer2D.COLOR.roundedRect(xb, yb, wb, hb, rb, 0.0f, bg);
|
||||
int gradLight = HudRenderUtil.mixColor(bg, 0xFFFFFFFF, 0.36f + 0.10f * hoverEase);
|
||||
int gradMid = HudRenderUtil.mixColor(bg, 0xFF000000, 0.03f);
|
||||
int gradDark = HudRenderUtil.mixColor(bg, 0xFF000000, 0.20f + 0.10f * hoverEase);
|
||||
if (hoverEase > 0.001f) {
|
||||
int accentTop = HudRenderUtil.mixColor(theme().accent(), bg, 0.65f);
|
||||
int accentBottom = HudRenderUtil.mixColor(theme().accentSoft(), bg, 0.75f);
|
||||
float t = Math.min(1.0f, hoverEase * 0.6f);
|
||||
gradLight = HudRenderUtil.mixColor(gradLight, accentTop, t);
|
||||
gradDark = HudRenderUtil.mixColor(gradDark, accentBottom, t * 0.85f);
|
||||
}
|
||||
Renderer2D.COLOR.roundedRectGradientQuad(xb, yb, wb, hb, rb, 0.0f, gradLight, gradMid, gradDark, gradMid);
|
||||
float strokeThickness = 0.7f * scale;
|
||||
int strokeInner = HudRenderUtil.scaleAlpha(strokeBase, 0.9f * alpha);
|
||||
int strokeTop = HudRenderUtil.mixColor(strokeInner, 0xFFFFFFFF, 0.2f);
|
||||
int strokeBottom = HudRenderUtil.mixColor(strokeInner, 0xFF000000, 0.25f);
|
||||
Renderer2D.COLOR.roundedRectStrokeGradient(xb, yb, wb, hb, rb, 0.0f, strokeThickness, strokeTop, strokeBottom, 90f);
|
||||
float halo = (focused ? 0.45f : 0.25f) * hoverEase;
|
||||
if (enabled && halo > 0.01f) {
|
||||
int haloColor = HudRenderUtil.scaleAlpha(strokeBase, 0.35f * halo * alpha);
|
||||
Renderer2D.COLOR.roundedRectStroke(xb, yb, wb, hb, rb, 0.0f, strokeThickness + 1.1f * scale, haloColor);
|
||||
}
|
||||
}
|
||||
private static void renderGradient(BetterButtons cfg, float xb, float yb, float wb, float hb, float rb, float softnessPx, float strokeSoftPx, float alpha, float hoverEase, boolean enabled, boolean focused, float scale) {
|
||||
int base = cfg.getGradientBaseColor().getArgb();
|
||||
int grad = cfg.getGradientColor().getArgb();
|
||||
float intensity = clamp01(cfg.getGradientIntensity().get());
|
||||
float mix = clamp01(intensity + (enabled ? hoverEase * 0.2f : 0.0f));
|
||||
|
||||
int start = HudRenderUtil.mixColor(base, grad, mix);
|
||||
int end = HudRenderUtil.mixColor(base, grad, intensity * 0.2f);
|
||||
if (!enabled) {
|
||||
start = HudRenderUtil.mixColor(start, 0xFF000000, 0.2f);
|
||||
end = HudRenderUtil.mixColor(end, 0xFF000000, 0.2f);
|
||||
}
|
||||
|
||||
int strokeBase = HudRenderUtil.mixColor(base, 0xFFFFFFFF, 0.18f);
|
||||
strokeBase = HudRenderUtil.mixColor(strokeBase, grad, 0.12f * hoverEase);
|
||||
if (!enabled) {
|
||||
strokeBase = HudRenderUtil.mixColor(strokeBase, 0xFF000000, 0.25f);
|
||||
}
|
||||
|
||||
int bgStart = HudRenderUtil.scaleAlpha(start, alpha);
|
||||
int bgEnd = HudRenderUtil.scaleAlpha(end, alpha);
|
||||
int stroke = HudRenderUtil.scaleAlpha(strokeBase, 0.85f * alpha);
|
||||
|
||||
Renderer2D.COLOR.roundedRectGradient(xb, yb, wb, hb, rb, softnessPx, bgStart, bgEnd, cfg.getGradientAngle().get());
|
||||
|
||||
float strokeThickness = 0.7f * scale;
|
||||
Renderer2D.COLOR.roundedRectStroke(xb, yb, wb, hb, rb, strokeSoftPx, strokeThickness, stroke);
|
||||
|
||||
float halo = (focused ? 0.45f : 0.25f) * hoverEase;
|
||||
if (enabled && halo > 0.01f) {
|
||||
int haloColor = HudRenderUtil.scaleAlpha(strokeBase, 0.35f * halo * alpha);
|
||||
Renderer2D.COLOR.roundedRectStroke(xb, yb, wb, hb, rb, strokeSoftPx, strokeThickness + 1.1f * scale, haloColor);
|
||||
}
|
||||
}
|
||||
|
||||
private static void renderCustom(BetterButtons cfg, float xb, float yb, float wb, float hb, float rb, float softnessPx, float strokeSoftPx, float alpha, float hoverEase, boolean enabled, boolean focused, float scale) {
|
||||
int base = cfg.getCustomBg().getArgb();
|
||||
int hoverBg = cfg.getCustomBgHover().getArgb();
|
||||
int disabledBg = cfg.getCustomBgDisabled().getArgb();
|
||||
int bg = enabled ? HudRenderUtil.mixColor(base, hoverBg, hoverEase) : disabledBg;
|
||||
|
||||
int strokeBase = enabled
|
||||
? HudRenderUtil.mixColor(cfg.getCustomStroke().getArgb(), cfg.getCustomStrokeHover().getArgb(), hoverEase)
|
||||
: cfg.getCustomStrokeDisabled().getArgb();
|
||||
|
||||
float strokeThickness = cfg.getCustomStrokeThickness().get() * scale;
|
||||
|
||||
if (cfg.getCustomGradient().get()) {
|
||||
float t = clamp01(cfg.getCustomGradientIntensity().get());
|
||||
int grad = cfg.getCustomGradientColor().getArgb();
|
||||
int start = HudRenderUtil.mixColor(bg, grad, t);
|
||||
int end = HudRenderUtil.mixColor(bg, grad, t * 0.2f);
|
||||
int bgStart = HudRenderUtil.scaleAlpha(start, alpha);
|
||||
int bgEnd = HudRenderUtil.scaleAlpha(end, alpha);
|
||||
Renderer2D.COLOR.roundedRectGradient(xb, yb, wb, hb, rb, softnessPx, bgStart, bgEnd, cfg.getCustomGradientAngle().get());
|
||||
} else {
|
||||
int fill = HudRenderUtil.scaleAlpha(bg, alpha);
|
||||
Renderer2D.COLOR.roundedRect(xb, yb, wb, hb, rb, softnessPx, fill);
|
||||
}
|
||||
|
||||
int stroke = HudRenderUtil.scaleAlpha(strokeBase, 0.85f * alpha);
|
||||
Renderer2D.COLOR.roundedRectStroke(xb, yb, wb, hb, rb, strokeSoftPx, strokeThickness, stroke);
|
||||
|
||||
float halo = (focused ? 0.45f : 0.25f) * hoverEase;
|
||||
if (enabled && halo > 0.01f) {
|
||||
int haloColor = HudRenderUtil.scaleAlpha(strokeBase, 0.35f * halo * alpha);
|
||||
Renderer2D.COLOR.roundedRectStroke(xb, yb, wb, hb, rb, strokeSoftPx, strokeThickness + 1.1f * scale, haloColor);
|
||||
}
|
||||
}
|
||||
|
||||
private static float clamp01(float value) {
|
||||
return Math.max(0.0f, Math.min(1.0f, value));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
/*
|
||||
* 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.buttons.render;
|
||||
import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil;
|
||||
import dev.loki.lovisual.features.gui.hud.nondraggable.impl.buttons.BetterButtons;
|
||||
import dev.loki.lovisual.render.engine.color.RenderColor;
|
||||
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.text.format.LegacyTextUtil;
|
||||
import dev.loki.lovisual.util.text.format.TextRenderUtil;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import static dev.loki.lovisual.features.theme.Theme.theme;
|
||||
/**
|
||||
* Текстовый слой кнопок: центрированный/левый выравниваемый, legacy-цвета, обрезка с "...".
|
||||
*/
|
||||
public final class ButtonTextRenderer {
|
||||
private static final float TEXT_SCALE_BASE = 0.42f;
|
||||
private static final float TEXT_PADDING_X = 6.0f;
|
||||
private static final float TEXT_Y_OFFSET = 0.0f;
|
||||
private ButtonTextRenderer() {
|
||||
}
|
||||
public static void renderCentered(Component text, float x, float y, float w, float h, float alpha, boolean enabled, float hover) {
|
||||
renderCentered(text, x, y, w, h, alpha, enabled, hover, true);
|
||||
}
|
||||
public static void renderCentered(Component text, float x, float y, float w, float h, float alpha, boolean enabled, float hover, boolean snapToPixel) {
|
||||
TextRenderer tr = Fonts.renderer("Monsterrat", FontInfo.Type.Regular, TextRenderer.get());
|
||||
if (tr == null) return;
|
||||
if (text == null || text.getString().isEmpty()) return;
|
||||
float padX = resolvePaddingX();
|
||||
float scale = resolveScaleBase() * (h / 20.0f);
|
||||
int baseText = resolveTextColor(enabled, hover);
|
||||
List<TextRenderUtil.FlatPart> parts = flattenLegacy(text, baseText);
|
||||
parts = trimParts(tr, parts, w - padX * 2f, scale, baseText);
|
||||
if (parts.isEmpty()) return;
|
||||
double tw = measurePartsWidth(tr, parts, scale);
|
||||
double th = measureHeight(tr, scale);
|
||||
float tx = (float) (x + (w - tw) * 0.5f);
|
||||
float ty = (float) (y + (h - th) * 0.5f + TEXT_Y_OFFSET);
|
||||
if (snapToPixel) {
|
||||
tx = Math.round(tx);
|
||||
ty = Math.round(ty);
|
||||
}
|
||||
tr.begin(scale, false, false);
|
||||
float cx = tx;
|
||||
for (TextRenderUtil.FlatPart part : parts) {
|
||||
if (part.text().isEmpty()) continue;
|
||||
int color = HudRenderUtil.scaleAlpha(part.color(), alpha);
|
||||
tr.render(part.text(), cx, ty, new RenderColor(color), false);
|
||||
cx += (float) tr.getWidth(part.text(), false);
|
||||
}
|
||||
tr.end();
|
||||
}
|
||||
public static void renderLeftAligned(Component text, float x0, float y0, float x1, float y1, float alpha, boolean enabled, float hover) {
|
||||
renderLeftAligned(text, x0, y0, x1, y1, alpha, enabled, hover, true);
|
||||
}
|
||||
public static void renderLeftAligned(Component text, float x0, float y0, float x1, float y1, float alpha, boolean enabled, float hover, boolean snapToPixel) {
|
||||
TextRenderer tr = Fonts.renderer("Monsterrat", FontInfo.Type.Regular, TextRenderer.get());
|
||||
if (tr == null) return;
|
||||
if (text == null || text.getString().isEmpty()) return;
|
||||
float w = x1 - x0;
|
||||
float h = y1 - y0;
|
||||
float padX = resolvePaddingX();
|
||||
float scale = resolveScaleBase() * (h / 20.0f);
|
||||
int baseText = resolveTextColor(enabled, hover);
|
||||
List<TextRenderUtil.FlatPart> parts = flattenLegacy(text, baseText);
|
||||
parts = trimParts(tr, parts, w - padX, scale, baseText);
|
||||
if (parts.isEmpty()) return;
|
||||
|
||||
double th = measureHeight(tr, scale);
|
||||
float tx = x0 + padX * 0.5f;
|
||||
float ty = (float) (y0 + (h - th) * 0.5f + TEXT_Y_OFFSET);
|
||||
if (snapToPixel) {
|
||||
tx = Math.round(tx);
|
||||
ty = Math.round(ty);
|
||||
}
|
||||
|
||||
tr.begin(scale, false, false);
|
||||
float cx = tx;
|
||||
for (TextRenderUtil.FlatPart part : parts) {
|
||||
if (part.text().isEmpty()) continue;
|
||||
int color = HudRenderUtil.scaleAlpha(part.color(), alpha);
|
||||
tr.render(part.text(), cx, ty, new RenderColor(color), false);
|
||||
cx += (float) tr.getWidth(part.text(), false);
|
||||
}
|
||||
tr.end();
|
||||
}
|
||||
|
||||
static float resolveScaleBase() {
|
||||
BetterButtons cfg = BetterButtons.get();
|
||||
if (cfg != null && cfg.getStyle().get() == BetterButtons.Style.CUSTOM) {
|
||||
return TEXT_SCALE_BASE * cfg.getCustomTextScale().get();
|
||||
}
|
||||
return TEXT_SCALE_BASE;
|
||||
}
|
||||
|
||||
static float resolvePaddingX() {
|
||||
BetterButtons cfg = BetterButtons.get();
|
||||
if (cfg != null && cfg.getStyle().get() == BetterButtons.Style.CUSTOM) {
|
||||
return cfg.getCustomTextPadX().get();
|
||||
}
|
||||
return TEXT_PADDING_X;
|
||||
}
|
||||
|
||||
static int resolveTextColor(boolean enabled, float hover) {
|
||||
BetterButtons cfg = BetterButtons.get();
|
||||
if (cfg != null && cfg.getStyle().get() == BetterButtons.Style.CUSTOM) {
|
||||
return enabled ? cfg.getCustomText().getArgb() : cfg.getCustomTextDisabled().getArgb();
|
||||
}
|
||||
return enabled ? theme().textPrimary() : theme().textMuted();
|
||||
}
|
||||
|
||||
private static List<TextRenderUtil.FlatPart> flattenLegacy(Component text, int defaultColor) {
|
||||
Component converted = LegacyTextUtil.convertLegacyCodes(text);
|
||||
return TextRenderUtil.flatten(converted, defaultColor);
|
||||
}
|
||||
|
||||
private static List<TextRenderUtil.FlatPart> trimParts(TextRenderer tr, List<TextRenderUtil.FlatPart> parts, float maxWidth, float scale, int defaultColor) {
|
||||
if (parts.isEmpty()) return parts;
|
||||
tr.begin(scale, true, false);
|
||||
try {
|
||||
double total = 0.0;
|
||||
for (TextRenderUtil.FlatPart part : parts) {
|
||||
if (!part.text().isEmpty()) {
|
||||
total += tr.getWidth(part.text(), false);
|
||||
}
|
||||
}
|
||||
if (total <= maxWidth) return parts;
|
||||
|
||||
String ellipsis = "...";
|
||||
double ellipsisWidth = tr.getWidth(ellipsis, false);
|
||||
if (maxWidth <= ellipsisWidth) {
|
||||
List<TextRenderUtil.FlatPart> out = new ArrayList<>();
|
||||
out.add(new TextRenderUtil.FlatPart(ellipsis, defaultColor));
|
||||
return out;
|
||||
}
|
||||
|
||||
List<TextRenderUtil.FlatPart> out = new ArrayList<>();
|
||||
double width = 0.0;
|
||||
for (TextRenderUtil.FlatPart part : parts) {
|
||||
String s = part.text();
|
||||
if (s.isEmpty()) continue;
|
||||
|
||||
double prev = 0.0;
|
||||
int keep = 0;
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
double cur = tr.getWidth(s, i + 1, false);
|
||||
double delta = cur - prev;
|
||||
if (width + delta + ellipsisWidth > maxWidth) {
|
||||
if (keep > 0) {
|
||||
out.add(new TextRenderUtil.FlatPart(s.substring(0, keep), part.color()));
|
||||
}
|
||||
out.add(new TextRenderUtil.FlatPart(ellipsis, defaultColor));
|
||||
return out;
|
||||
}
|
||||
width += delta;
|
||||
prev = cur;
|
||||
keep = i + 1;
|
||||
}
|
||||
|
||||
out.add(new TextRenderUtil.FlatPart(s.substring(0, keep), part.color()));
|
||||
}
|
||||
|
||||
out.add(new TextRenderUtil.FlatPart(ellipsis, defaultColor));
|
||||
return out;
|
||||
} finally {
|
||||
tr.end();
|
||||
}
|
||||
}
|
||||
|
||||
private static double measurePartsWidth(TextRenderer tr, List<TextRenderUtil.FlatPart> parts, float scale) {
|
||||
tr.begin(scale, true, false);
|
||||
try {
|
||||
double width = 0.0;
|
||||
for (TextRenderUtil.FlatPart part : parts) {
|
||||
if (!part.text().isEmpty()) {
|
||||
width += tr.getWidth(part.text(), false);
|
||||
}
|
||||
}
|
||||
return width;
|
||||
} finally {
|
||||
tr.end();
|
||||
}
|
||||
}
|
||||
|
||||
private static double measureHeight(TextRenderer tr, float scale) {
|
||||
tr.begin(scale, true, false);
|
||||
try {
|
||||
return tr.getHeight(false);
|
||||
} finally {
|
||||
tr.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.nondraggable.impl.buttons.state;
|
||||
|
||||
import net.minecraft.client.gui.components.AbstractWidget;
|
||||
import net.minecraft.util.Util;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
/**
|
||||
* Hover-анимация кнопок: хранит прогресс наведения и позицию курсора.
|
||||
*/
|
||||
public final class ButtonHoverState {
|
||||
private static final float HOVER_SPEED_IN = 20.0f;
|
||||
private static final float HOVER_SPEED_OUT = 16.0f;
|
||||
|
||||
private static final Map<AbstractWidget, Float> HOVER = new WeakHashMap<>();
|
||||
private static final Map<AbstractWidget, Long> LAST_TICK = new WeakHashMap<>();
|
||||
private static final Map<AbstractWidget, Float> MOUSE_X = new WeakHashMap<>();
|
||||
private static final Map<AbstractWidget, Float> MOUSE_Y = new WeakHashMap<>();
|
||||
|
||||
private ButtonHoverState() {
|
||||
}
|
||||
|
||||
public static void clear(AbstractWidget widget) {
|
||||
if (widget == null) return;
|
||||
HOVER.remove(widget);
|
||||
LAST_TICK.remove(widget);
|
||||
MOUSE_X.remove(widget);
|
||||
MOUSE_Y.remove(widget);
|
||||
}
|
||||
|
||||
public static void clearAll() {
|
||||
HOVER.clear();
|
||||
LAST_TICK.clear();
|
||||
MOUSE_X.clear();
|
||||
MOUSE_Y.clear();
|
||||
}
|
||||
|
||||
public static float updateHover(AbstractWidget widget, boolean hovered) {
|
||||
if (widget != null) {
|
||||
return updateHover(widget, hovered, widget.getX() + widget.getWidth() * 0.5f, widget.getY() + widget.getHeight() * 0.5f);
|
||||
}
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
public static float updateHover(AbstractWidget widget, boolean hovered, float mouseX, float mouseY) {
|
||||
if (widget == null) return 0.0f;
|
||||
if (hovered) {
|
||||
MOUSE_X.put(widget, mouseX);
|
||||
MOUSE_Y.put(widget, mouseY);
|
||||
}
|
||||
|
||||
float target = hovered ? 1.0f : 0.0f;
|
||||
float current = HOVER.getOrDefault(widget, 0.0f);
|
||||
|
||||
long now = Util.getMillis();
|
||||
long last = LAST_TICK.getOrDefault(widget, now);
|
||||
LAST_TICK.put(widget, now);
|
||||
|
||||
float dt = Math.min(0.05f, (now - last) / 1000.0f);
|
||||
float speed = hovered ? HOVER_SPEED_IN : HOVER_SPEED_OUT;
|
||||
float lerp = 1.0f - (float) Math.exp(-speed * dt);
|
||||
current = current + (target - current) * lerp;
|
||||
|
||||
HOVER.put(widget, current);
|
||||
return current;
|
||||
}
|
||||
|
||||
public static float smoothHover(float hover) {
|
||||
float h = Math.max(0.0f, Math.min(1.0f, hover));
|
||||
return h * h * (3.0f - 2.0f * h);
|
||||
}
|
||||
|
||||
public static float mouseX(AbstractWidget widget, float fallback) {
|
||||
return MOUSE_X.getOrDefault(widget, fallback);
|
||||
}
|
||||
|
||||
public static float mouseY(AbstractWidget widget, float fallback) {
|
||||
return MOUSE_Y.getOrDefault(widget, fallback);
|
||||
}
|
||||
|
||||
public static float currentHover(AbstractWidget widget) {
|
||||
return HOVER.getOrDefault(widget, 0.0f);
|
||||
}
|
||||
}
|
||||
|
|
@ -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.gui.hud.nondraggable.impl.buttons.state;
|
||||
|
||||
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
|
||||
import dev.loki.lovisual.render.engine.renderer.warp.RenderWarpStack;
|
||||
import net.minecraft.client.gui.components.AbstractWidget;
|
||||
|
||||
/**
|
||||
* Параллакс кнопок по курсору: поворот и сдвиг контента при наведении.
|
||||
*/
|
||||
public final class ButtonParallax {
|
||||
private static final float PARALLAX_MAX_ANGLE = 7.0f;
|
||||
private static final float PARALLAX_DEPTH = 3.5f;
|
||||
private static final float PARALLAX_PERSPECTIVE = 1.0f;
|
||||
private static final float PARALLAX_SCALE_BOOST = 0.018f;
|
||||
private static final float PARALLAX_CONTENT_SHIFT = 1.35f;
|
||||
|
||||
private ButtonParallax() {
|
||||
}
|
||||
|
||||
public static Data compute(AbstractWidget widget, float hover) {
|
||||
if (widget == null || hover <= 0.001f || !widget.isActive()) {
|
||||
return Data.NONE;
|
||||
}
|
||||
|
||||
float w = Math.max(1.0f, widget.getWidth());
|
||||
float h = Math.max(1.0f, widget.getHeight());
|
||||
float mx = ButtonHoverState.mouseX(widget, widget.getX() + w * 0.5f);
|
||||
float my = ButtonHoverState.mouseY(widget, widget.getY() + h * 0.5f);
|
||||
float nx = clamp((mx - widget.getX()) / w * 2.0f - 1.0f, -1.0f, 1.0f);
|
||||
float ny = clamp((my - widget.getY()) / h * 2.0f - 1.0f, -1.0f, 1.0f);
|
||||
float ease = ButtonHoverState.smoothHover(hover);
|
||||
if (ease <= 0.001f) {
|
||||
return Data.NONE;
|
||||
}
|
||||
|
||||
return new Data(
|
||||
-nx * PARALLAX_MAX_ANGLE * ease,
|
||||
ny * PARALLAX_MAX_ANGLE * ease,
|
||||
0.0f,
|
||||
1.0f + PARALLAX_SCALE_BOOST * ease,
|
||||
nx * PARALLAX_CONTENT_SHIFT * ease,
|
||||
ny * PARALLAX_CONTENT_SHIFT * ease,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
public static float shiftX(AbstractWidget widget, float hover) {
|
||||
return compute(widget, hover).shiftX;
|
||||
}
|
||||
|
||||
public static float shiftY(AbstractWidget widget, float hover) {
|
||||
return compute(widget, hover).shiftY;
|
||||
}
|
||||
|
||||
public static RenderWarpStack.Scope pushWarp(Data parallax, float x, float y, float w, float h) {
|
||||
if (parallax == null || !parallax.active || Math.abs(w) <= 0.0001f || Math.abs(h) <= 0.0001f) {
|
||||
return Renderer2D.pushWarp(null);
|
||||
}
|
||||
return Renderer2D.pushPerspectiveWarp(
|
||||
x, y, w, h,
|
||||
parallax.yawDeg, parallax.pitchDeg, parallax.rollDeg,
|
||||
PARALLAX_DEPTH, PARALLAX_PERSPECTIVE, parallax.scale
|
||||
);
|
||||
}
|
||||
|
||||
private static float clamp(float value, float min, float max) {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
public record Data(float yawDeg,
|
||||
float pitchDeg,
|
||||
float rollDeg,
|
||||
float scale,
|
||||
float shiftX,
|
||||
float shiftY,
|
||||
boolean active) {
|
||||
public static final Data NONE = new Data(0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, false);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
/*
|
||||
* 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.buttons.state;
|
||||
import dev.loki.lovisual.features.gui.hud.nondraggable.impl.tooltips.BetterTooltips;
|
||||
import dev.loki.lovisual.mixins.accessors.widget.ClickableWidgetAccessor;
|
||||
import dev.loki.lovisual.render.engine.core.context.ViewportContext;
|
||||
import dev.loki.lovisual.render.helpers.clip.ScissorFunction;
|
||||
import dev.loki.lovisual.util.screen.ClientScreen;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.gui.components.*;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.gui.screens.inventory.tooltip.DefaultTooltipPositioner;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.util.FormattedCharSequence;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Deque;
|
||||
import java.util.List;
|
||||
/**
|
||||
* Очередь отложенного рендера кнопок (batch за кадр + scissor-контекст).
|
||||
*/
|
||||
public final class ButtonQueue {
|
||||
private static final List<RenderCall> QUEUE = new ArrayList<>();
|
||||
private static final Deque<ScissorRect> SCISSOR_STACK = new ArrayDeque<>();
|
||||
private static GuiGraphicsExtractor LAST_CTX;
|
||||
private static Screen LAST_SCREEN;
|
||||
private ButtonQueue() {
|
||||
}
|
||||
public static void beginFrame(GuiGraphicsExtractor ctx) {
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
Screen screen = mc != null ? ClientScreen.current() : null;
|
||||
if (screen != LAST_SCREEN) {
|
||||
ButtonHoverState.clearAll();
|
||||
LAST_SCREEN = screen;
|
||||
}
|
||||
LAST_CTX = ctx;
|
||||
QUEUE.clear();
|
||||
SCISSOR_STACK.clear();
|
||||
BetterTooltips.beginTooltipFrame();
|
||||
}
|
||||
public static GuiGraphicsExtractor lastContext() {
|
||||
return LAST_CTX;
|
||||
}
|
||||
public static void pushScissorContext(int x1, int y1, int x2, int y2) {
|
||||
if (x2 <= x1 || y2 <= y1) return;
|
||||
SCISSOR_STACK.push(new ScissorRect(x1, y1, x2, y2));
|
||||
}
|
||||
|
||||
public static void popScissorContext() {
|
||||
if (!SCISSOR_STACK.isEmpty()) {
|
||||
SCISSOR_STACK.pop();
|
||||
}
|
||||
}
|
||||
|
||||
private static ScissorRect currentScissor() {
|
||||
return SCISSOR_STACK.peek();
|
||||
}
|
||||
|
||||
public static void flush() {
|
||||
if (QUEUE.isEmpty()) return;
|
||||
GuiGraphicsExtractor ctx = LAST_CTX;
|
||||
if (ctx == null) {
|
||||
QUEUE.clear();
|
||||
return;
|
||||
}
|
||||
for (RenderCall call : QUEUE) {
|
||||
ScissorRect scissor = extractScissor(call);
|
||||
boolean pushed = pushScissor(scissor);
|
||||
call.render(ctx);
|
||||
if (pushed) {
|
||||
popScissor();
|
||||
}
|
||||
}
|
||||
QUEUE.clear();
|
||||
}
|
||||
|
||||
private static ScissorRect extractScissor(RenderCall call) {
|
||||
if (call instanceof ButtonCall c) return c.scissor();
|
||||
if (call instanceof SliderCall c) return c.scissor();
|
||||
if (call instanceof TextIconCall c) return c.scissor();
|
||||
if (call instanceof CyclingCall c) return c.scissor();
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean pushScissor(ScissorRect scissor) {
|
||||
if (scissor == null) return false;
|
||||
float scale = ViewportContext.getScaleFactor();
|
||||
return ScissorFunction.pushScaledRect(scissor.x1, scissor.y1, scissor.x2, scissor.y2, scale);
|
||||
}
|
||||
|
||||
private static void popScissor() {
|
||||
ScissorFunction.pop();
|
||||
}
|
||||
|
||||
public static void captureTooltip(AbstractWidget widget, int mouseX, int mouseY) {
|
||||
if (widget == null) return;
|
||||
BetterTooltips tooltips = BetterTooltips.get();
|
||||
if (tooltips == null || !tooltips.useCustomGuiTooltips()) {
|
||||
return;
|
||||
}
|
||||
WidgetTooltipHolder state = ((ClickableWidgetAccessor) widget).lovisual$getTooltipState();
|
||||
if (state == null) return;
|
||||
Tooltip tooltip = state.get();
|
||||
if (tooltip == null) return;
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
if (mc == null) return;
|
||||
List<FormattedCharSequence> lines = tooltip.toCharSequence(mc);
|
||||
if (lines == null || lines.isEmpty()) return;
|
||||
BetterTooltips.captureTooltipOrdered(lines, DefaultTooltipPositioner.INSTANCE, mouseX, mouseY);
|
||||
}
|
||||
|
||||
public static boolean hasPending() {
|
||||
return !QUEUE.isEmpty();
|
||||
}
|
||||
|
||||
public static boolean hasTooltip() {
|
||||
return BetterTooltips.hasTooltip();
|
||||
}
|
||||
|
||||
public static void enqueueButton(AbstractWidget widget, Component text, float hover, boolean enabled, boolean focused) {
|
||||
QUEUE.add(new ButtonCall(widget, text, hover, enabled, focused, currentScissor()));
|
||||
}
|
||||
|
||||
public static void enqueueSlider(AbstractWidget widget, float value, boolean dragging, float hover) {
|
||||
QUEUE.add(new SliderCall(widget, value, dragging, hover, currentScissor()));
|
||||
}
|
||||
|
||||
public static void enqueueTextIconButton(SpriteIconButton widget, boolean iconOnly, float hover, boolean enabled, boolean focused) {
|
||||
QUEUE.add(new TextIconCall(widget, iconOnly, hover, enabled, focused, currentScissor()));
|
||||
}
|
||||
|
||||
public static void enqueueCyclingButton(CycleButton<?> widget, Identifier icon, boolean showLabel, float hover, boolean enabled, boolean focused) {
|
||||
QUEUE.add(new CyclingCall(widget, icon, showLabel, hover, enabled, focused, currentScissor()));
|
||||
}
|
||||
|
||||
public static void renderTooltip() {
|
||||
BetterTooltips.renderTooltipWithContext(LAST_CTX);
|
||||
}
|
||||
|
||||
public interface RenderCall {
|
||||
void render(GuiGraphicsExtractor ctx);
|
||||
}
|
||||
|
||||
public record ButtonCall(AbstractWidget widget,
|
||||
Component text,
|
||||
float hover,
|
||||
boolean enabled,
|
||||
boolean focused,
|
||||
ScissorRect scissor) implements RenderCall {
|
||||
@Override
|
||||
public void render(GuiGraphicsExtractor ctx) {
|
||||
dev.loki.lovisual.features.gui.hud.nondraggable.impl.buttons.BetterButtons.renderButton(ctx, widget, text, hover, enabled, focused);
|
||||
}
|
||||
}
|
||||
|
||||
public record SliderCall(AbstractWidget widget,
|
||||
float value,
|
||||
boolean dragging,
|
||||
float hover,
|
||||
ScissorRect scissor) implements RenderCall {
|
||||
@Override
|
||||
public void render(GuiGraphicsExtractor ctx) {
|
||||
dev.loki.lovisual.features.gui.hud.nondraggable.impl.buttons.BetterButtons.renderSlider(ctx, widget, value, dragging, hover);
|
||||
}
|
||||
}
|
||||
|
||||
public record TextIconCall(SpriteIconButton widget,
|
||||
boolean iconOnly,
|
||||
float hover,
|
||||
boolean enabled,
|
||||
boolean focused,
|
||||
ScissorRect scissor) implements RenderCall {
|
||||
@Override
|
||||
public void render(GuiGraphicsExtractor ctx) {
|
||||
dev.loki.lovisual.features.gui.hud.nondraggable.impl.buttons.BetterButtons.renderTextIconButton(ctx, widget, iconOnly, hover, enabled, focused);
|
||||
}
|
||||
}
|
||||
|
||||
public record CyclingCall(CycleButton<?> widget,
|
||||
Identifier icon,
|
||||
boolean showLabel,
|
||||
float hover,
|
||||
boolean enabled,
|
||||
boolean focused,
|
||||
ScissorRect scissor) implements RenderCall {
|
||||
@Override
|
||||
public void render(GuiGraphicsExtractor ctx) {
|
||||
dev.loki.lovisual.features.gui.hud.nondraggable.impl.buttons.BetterButtons.renderCyclingButton(ctx, widget, icon, showLabel, hover, enabled, focused);
|
||||
}
|
||||
}
|
||||
|
||||
public record ScissorRect(int x1, int y1, int x2, int y2) {
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ 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.TextGlyphFallback;
|
||||
import dev.loki.lovisual.render.engine.text.glyph.custom.TextGlyphFallback;
|
||||
import dev.loki.lovisual.render.engine.text.render.TextRenderer;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.Style;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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.misc.esp;
|
||||
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import dev.loki.lovisual.config.values.collection.ItemIdSetValue;
|
||||
import dev.loki.lovisual.config.values.color.RGBAColorValue;
|
||||
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.core.ModuleCategory;
|
||||
import dev.loki.lovisual.features.module.core.ModuleInfo;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Подсветка слотов инвентаря по группам предметов (замена ShitDropper-выброса).
|
||||
* Каждая группа — список item id + цвет. Рендер через AbstractContainerScreenMixin.
|
||||
*/
|
||||
@ModuleInfo(id = "shitesp", displayName = "ShitESP", category = ModuleCategory.MISC)
|
||||
public class ShitEsp extends Module {
|
||||
|
||||
private final ItemIdSetValue groupA = itemList("shitEspGroupA", "group_a", TextListSetting.PickerMode.ALL);
|
||||
private final RGBAColorValue colorA = color("shitEspColorA", "color_a", "#80FF3B30");
|
||||
|
||||
private final ItemIdSetValue groupB = itemList("shitEspGroupB", "group_b", TextListSetting.PickerMode.ALL);
|
||||
private final RGBAColorValue colorB = color("shitEspColorB", "color_b", "#8030FF9A");
|
||||
|
||||
private final ItemIdSetValue groupC = itemList("shitEspGroupC", "group_c", TextListSetting.PickerMode.ALL);
|
||||
private final RGBAColorValue colorC = color("shitEspColorC", "color_c", "#80309CFF");
|
||||
|
||||
public Integer colorFor(ItemStack stack) {
|
||||
if (stack == null || stack.isEmpty()) return null;
|
||||
String id = BuiltInRegistries.ITEM.getKey(stack.getItem()).toString().toLowerCase();
|
||||
Set<String> a = groupA.get();
|
||||
if (a != null && a.contains(id)) return colorA.getArgb();
|
||||
Set<String> b = groupB.get();
|
||||
if (b != null && b.contains(id)) return colorB.getArgb();
|
||||
Set<String> c = groupC.get();
|
||||
if (c != null && c.contains(id)) return colorC.getArgb();
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean hasAnyGroup() {
|
||||
Set<String> a = groupA.get();
|
||||
Set<String> b = groupB.get();
|
||||
Set<String> c = groupC.get();
|
||||
return (a != null && !a.isEmpty()) || (b != null && !b.isEmpty()) || (c != null && !c.isEmpty());
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.module.modules.misc;
|
||||
package dev.loki.lovisual.features.module.modules.misc.vanilla;
|
||||
|
||||
import dev.loki.lovisual.config.values.primitive.BooleanValue;
|
||||
import dev.loki.lovisual.features.module.core.Module;
|
||||
|
|
@ -21,7 +21,6 @@ import dev.loki.lovisual.features.module.core.ModuleCategory;
|
|||
import dev.loki.lovisual.features.module.core.ModuleInfo;
|
||||
import dev.loki.lovisual.mixins.accessors.misc.MultiPlayerGameModeAccessor;
|
||||
import dev.loki.lovisual.mixins.accessors.inventory.PlayerInventoryAccessor;
|
||||
import dev.loki.lovisual.util.player.inventory.core.InventorySwap;
|
||||
|
||||
//todo Description
|
||||
@ModuleInfo(
|
||||
|
|
@ -42,7 +41,8 @@ public class AutoTool extends Module {
|
|||
public void onDisable() {
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
if (mc != null && mc.player != null && originalSlot >= 0) {
|
||||
InventorySwap.INSTANCE.selectHotbar(originalSlot);
|
||||
((PlayerInventoryAccessor) mc.player.getInventory()).lovisual$setSelectedSlot(originalSlot);
|
||||
syncSlot(originalSlot);
|
||||
}
|
||||
originalSlot = -1;
|
||||
lastBreakMs = 0L;
|
||||
|
|
@ -66,7 +66,8 @@ public class AutoTool extends Module {
|
|||
if (originalSlot < 0) {
|
||||
originalSlot = selected;
|
||||
}
|
||||
InventorySwap.INSTANCE.selectHotbar(bestSlot);
|
||||
((PlayerInventoryAccessor) mc.player.getInventory()).lovisual$setSelectedSlot(bestSlot);
|
||||
syncSlot(bestSlot);
|
||||
}
|
||||
lastBreakMs = GameClock.millis();
|
||||
return;
|
||||
|
|
@ -76,7 +77,8 @@ public class AutoTool extends Module {
|
|||
if (originalSlot >= 0) {
|
||||
long now = GameClock.millis();
|
||||
if (now - lastBreakMs >= restoreDelayMs.get()) {
|
||||
InventorySwap.INSTANCE.selectHotbar(originalSlot);
|
||||
((PlayerInventoryAccessor) mc.player.getInventory()).lovisual$setSelectedSlot(originalSlot);
|
||||
syncSlot(originalSlot);
|
||||
originalSlot = -1;
|
||||
}
|
||||
}
|
||||
|
|
@ -106,4 +108,13 @@ public class AutoTool extends Module {
|
|||
originalSlot = -1;
|
||||
lastBreakMs = 0L;
|
||||
}
|
||||
|
||||
private void syncSlot(int slot) {
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
if (mc.getConnection() == null) return;
|
||||
if (mc.gameMode instanceof MultiPlayerGameModeAccessor accessor) {
|
||||
accessor.lovisual$setLastSelectedSlot(slot);
|
||||
}
|
||||
mc.getConnection().send(new net.minecraft.network.protocol.game.ServerboundSetCarriedItemPacket(slot));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,8 +17,6 @@ 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.mixins.accessors.inventory.PlayerInventoryAccessor;
|
||||
import dev.loki.lovisual.util.player.interaction.ResetAttackCooldown;
|
||||
import dev.loki.lovisual.util.player.inventory.core.InventorySwap;
|
||||
|
||||
//todo Description
|
||||
@ModuleInfo(
|
||||
|
|
@ -29,8 +27,6 @@ import dev.loki.lovisual.util.player.inventory.core.InventorySwap;
|
|||
public class ClickPearl extends Module {
|
||||
private static final String ACTION_CLICKPEARL = "clickpearl";
|
||||
|
||||
private ItemStack previousHeld = ItemStack.EMPTY;
|
||||
|
||||
public ClickPearl() {
|
||||
// Default binds:
|
||||
// - Toggle module with LEFT_CTRL+Z
|
||||
|
|
@ -79,49 +75,8 @@ public class ClickPearl extends Module {
|
|||
return;
|
||||
}
|
||||
|
||||
int invPearl = findPearlInInventory(player);
|
||||
if (invPearl == -1) return;
|
||||
|
||||
int emptyHotbar = findFirstEmptyHotbarSlot(player);
|
||||
var handler = player.inventoryMenu;
|
||||
if (handler == null) return;
|
||||
int syncId = handler.containerId;
|
||||
|
||||
client.execute(() -> {
|
||||
if (emptyHotbar != -1) {
|
||||
int pearlScreen = InventorySwap.mapInventoryToScreenSlot(invPearl);
|
||||
int targetScreen = InventorySwap.mapHotbarToScreenSlot(emptyHotbar);
|
||||
|
||||
InventorySwap.INSTANCE.swapScreenSlots(pearlScreen, targetScreen);
|
||||
|
||||
inv.lovisual$setSelectedSlot(emptyHotbar);
|
||||
manager.useItem(player, InteractionHand.MAIN_HAND);
|
||||
player.swing(InteractionHand.MAIN_HAND);
|
||||
inv.lovisual$setSelectedSlot(originalSlot);
|
||||
} else {
|
||||
int targetScreen = InventorySwap.mapHotbarToScreenSlot(originalSlot);
|
||||
int pearlScreen = InventorySwap.mapInventoryToScreenSlot(invPearl);
|
||||
|
||||
ItemStack currentHeld = player.getMainHandItem();
|
||||
previousHeld = currentHeld.isEmpty() ? ItemStack.EMPTY : currentHeld.copy();
|
||||
|
||||
InventorySwap.INSTANCE.swapScreenSlots(pearlScreen, targetScreen);
|
||||
|
||||
manager.useItem(player, InteractionHand.MAIN_HAND);
|
||||
player.swing(InteractionHand.MAIN_HAND);
|
||||
|
||||
if (!previousHeld.isEmpty()) {
|
||||
int restoreSlot = findItemInInventory(player, previousHeld);
|
||||
if (restoreSlot != -1) {
|
||||
int restoreScreen = InventorySwap.mapInventoryToScreenSlot(restoreSlot);
|
||||
InventorySwap.INSTANCE.swapScreenSlots(restoreScreen, targetScreen);
|
||||
ResetAttackCooldown.resetAttackCooldown(player);
|
||||
}
|
||||
}
|
||||
|
||||
inv.lovisual$setSelectedSlot(originalSlot);
|
||||
}
|
||||
});
|
||||
// Inventory swap removed (hotbar-only mode) — pearl must be in hotbar/offhand
|
||||
return;
|
||||
}
|
||||
|
||||
private boolean isPearl(ItemStack stack) {
|
||||
|
|
@ -135,26 +90,4 @@ public class ClickPearl extends Module {
|
|||
return -1;
|
||||
}
|
||||
|
||||
private int findPearlInInventory(LocalPlayer player) {
|
||||
for (int i = 0; i < 36; i++) {
|
||||
if (isPearl(player.getInventory().getItem(i))) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private int findFirstEmptyHotbarSlot(LocalPlayer player) {
|
||||
for (int i = 0; i < 9; i++) {
|
||||
if (player.getInventory().getItem(i).isEmpty()) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private int findItemInInventory(LocalPlayer player, ItemStack itemStack) {
|
||||
if (itemStack == null || itemStack.isEmpty()) return -1;
|
||||
for (int i = 0; i < 36; i++) {
|
||||
ItemStack s = player.getInventory().getItem(i);
|
||||
if (!s.isEmpty() && s.getItem() == itemStack.getItem()) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,135 +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.player.inventory;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import dev.loki.lovisual.config.values.collection.ItemIdSetValue;
|
||||
import dev.loki.lovisual.config.values.primitive.NumberValue;
|
||||
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.core.ModuleCategory;
|
||||
import dev.loki.lovisual.features.module.core.ModuleInfo;
|
||||
import dev.loki.lovisual.util.player.inventory.core.InventorySwap;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
//todo Description
|
||||
@ModuleInfo(
|
||||
id = "shitdropper",
|
||||
displayName = "ShitDropper",
|
||||
category = ModuleCategory.PLAYER
|
||||
)
|
||||
public class ShitDropper extends Module {
|
||||
|
||||
private static final String SETTING_TRASH_ITEM_IDS = "trash_item_ids";
|
||||
private static final String SETTING_MAX_TRASH_SLOTS = "max_trash_slots";
|
||||
private static final String SETTING_STACKS_PER_TICK = "stacks_per_tick";
|
||||
private static final String SETTING_DELAY_TICKS = "delay_ticks";
|
||||
private final ItemIdSetValue trashItemsValue =
|
||||
itemList("shitDropperTrashItems", SETTING_TRASH_ITEM_IDS, TextListSetting.PickerMode.ALL);
|
||||
|
||||
/**
|
||||
* How many trash stacks are allowed to exist simultaneously in occupied slots. 0 = drop all.
|
||||
*/
|
||||
private final NumberValue<Integer> maxTrashSlotsValue = num(
|
||||
"shitDropperMaxTrashSlots",
|
||||
SETTING_MAX_TRASH_SLOTS,
|
||||
0,
|
||||
0,
|
||||
36
|
||||
);
|
||||
|
||||
/**
|
||||
* Maximum amount of stacks to drop per tick (to reduce server spam).
|
||||
*/
|
||||
private final NumberValue<Integer> stacksPerTickValue = num(
|
||||
"shitDropperStacksPerTick",
|
||||
SETTING_STACKS_PER_TICK,
|
||||
1,
|
||||
1,
|
||||
9
|
||||
);
|
||||
|
||||
/**
|
||||
* Delay between drop batches (ticks).
|
||||
*/
|
||||
private final NumberValue<Integer> delayTicksValue = num(
|
||||
"shitDropperDelayTicks",
|
||||
SETTING_DELAY_TICKS,
|
||||
2,
|
||||
0,
|
||||
40
|
||||
);
|
||||
|
||||
private int cooldownTicks;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
cooldownTicks = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTick() {
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
if (!isEnabled() || mc.player == null || mc.gameMode == null) return;
|
||||
|
||||
var handler = mc.player.containerMenu;
|
||||
if (handler == null) return;
|
||||
|
||||
if (!handler.getCarried().isEmpty()) return;
|
||||
|
||||
if (cooldownTicks > 0) {
|
||||
cooldownTicks--;
|
||||
return;
|
||||
}
|
||||
|
||||
var targets = trashItemsValue.get();
|
||||
if (targets == null || targets.isEmpty()) return;
|
||||
|
||||
// Ищем мусор только в PlayerInventory именно нашего игрока, индексы 0..35
|
||||
List<Integer> trashSlots = new ArrayList<>();
|
||||
|
||||
for (int screenSlot = 0; screenSlot < handler.slots.size(); screenSlot++) {
|
||||
var slot = handler.slots.get(screenSlot);
|
||||
if (slot == null || !slot.hasItem()) continue;
|
||||
|
||||
if (!(slot.container instanceof net.minecraft.world.entity.player.Inventory inv)) continue;
|
||||
if (inv.player != mc.player) continue;
|
||||
|
||||
int invIndex = slot.getContainerSlot();
|
||||
if (invIndex < 0 || invIndex >= 36) continue;
|
||||
|
||||
var stack = slot.getItem();
|
||||
if (stack == null || stack.isEmpty()) continue;
|
||||
|
||||
String id = BuiltInRegistries.ITEM.getKey(stack.getItem()).toString().toLowerCase();
|
||||
if (targets.contains(id)) {
|
||||
trashSlots.add(screenSlot);
|
||||
}
|
||||
}
|
||||
|
||||
int allowed = Math.max(0, maxTrashSlotsValue.get());
|
||||
int over = trashSlots.size() - allowed;
|
||||
if (over <= 0) return;
|
||||
|
||||
int perTick = Math.max(1, stacksPerTickValue.get());
|
||||
int toDrop = Math.min(perTick, over);
|
||||
|
||||
for (int i = 0; i < toDrop; i++) {
|
||||
var slot = handler.slots.get(trashSlots.get(i));
|
||||
|
||||
InventorySwap.INSTANCE.dropStack(slot);
|
||||
|
||||
}
|
||||
|
||||
cooldownTicks = Math.max(0, delayTicksValue.get());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.module.modules.visuals.effects;
|
||||
package dev.loki.lovisual.features.module.modules.visuals.effects.ambient;
|
||||
|
||||
import com.mojang.blaze3d.pipeline.RenderPipeline;
|
||||
import dev.loki.lovisual.config.values.color.RGBAColorValue;
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.module.modules.visuals.effects;
|
||||
package dev.loki.lovisual.features.module.modules.visuals.effects.ambient;
|
||||
|
||||
import dev.loki.lovisual.util.time.GameClock;
|
||||
|
||||
|
|
@ -38,6 +38,8 @@ 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;
|
||||
|
||||
|
|
@ -165,186 +167,6 @@ public class WorldParticles extends Module {
|
|||
return pos.add(right.x() + up.x(), right.y() + up.y(), right.z() + up.z());
|
||||
}
|
||||
|
||||
private 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);
|
||||
}
|
||||
|
||||
private static void addBillboardQuad(MeshBuilder mesh, double cx, double cy, double cz,
|
||||
float size, Quaternionf camRot, float rollRadians, int argb) {
|
||||
addBillboardQuadGradient(mesh, cx, cy, cz, size, camRot, rollRadians, argb, argb, argb, argb);
|
||||
}
|
||||
|
||||
private static void addBillboardQuadGradient(MeshBuilder mesh, double cx, double cy, double cz,
|
||||
float size, Quaternionf camRot, float rollRadians,
|
||||
int bottomLeftArgb,
|
||||
int bottomRightArgb,
|
||||
int topRightArgb,
|
||||
int topLeftArgb) {
|
||||
Vector3f baseRight = new Vector3f(1.0f, 0.0f, 0.0f).rotate(camRot);
|
||||
Vector3f baseUp = new Vector3f(0.0f, 1.0f, 0.0f).rotate(camRot);
|
||||
float cos = (float) Math.cos(rollRadians);
|
||||
float sin = (float) Math.sin(rollRadians);
|
||||
|
||||
Vector3f right = new Vector3f(baseRight).mul(cos).add(new Vector3f(baseUp).mul(sin)).mul(size);
|
||||
Vector3f up = new Vector3f(baseUp).mul(cos).sub(new Vector3f(baseRight).mul(sin)).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.0, 1.0).color(new RenderColor(bottomLeftArgb)).next();
|
||||
int i2 = mesh.vec3(p2x, p2y, p2z).vec2(1.0, 1.0).color(new RenderColor(bottomRightArgb)).next();
|
||||
int i3 = mesh.vec3(p3x, p3y, p3z).vec2(1.0, 0.0).color(new RenderColor(topRightArgb)).next();
|
||||
int i4 = mesh.vec3(p4x, p4y, p4z).vec2(0.0, 0.0).color(new RenderColor(topLeftArgb)).next();
|
||||
mesh.quad(i1, i2, i3, i4);
|
||||
}
|
||||
|
||||
private static void addBillboardRect(MeshBuilder mesh, double cx, double cy, double cz,
|
||||
float halfWidth, float halfHeight, Quaternionf camRot,
|
||||
float rollRadians, int argb) {
|
||||
Vector3f baseRight = new Vector3f(1.0f, 0.0f, 0.0f).rotate(camRot);
|
||||
Vector3f baseUp = new Vector3f(0.0f, 1.0f, 0.0f).rotate(camRot);
|
||||
float cos = (float) Math.cos(rollRadians);
|
||||
float sin = (float) Math.sin(rollRadians);
|
||||
|
||||
Vector3f right = new Vector3f(baseRight).mul(cos).add(new Vector3f(baseUp).mul(sin)).mul(halfWidth);
|
||||
Vector3f up = new Vector3f(baseUp).mul(cos).sub(new Vector3f(baseRight).mul(sin)).mul(halfHeight);
|
||||
|
||||
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.0, 1.0).color(new RenderColor(argb)).next();
|
||||
int i2 = mesh.vec3(p2x, p2y, p2z).vec2(1.0, 1.0).color(new RenderColor(argb)).next();
|
||||
int i3 = mesh.vec3(p3x, p3y, p3z).vec2(1.0, 0.0).color(new RenderColor(argb)).next();
|
||||
int i4 = mesh.vec3(p4x, p4y, p4z).vec2(0.0, 0.0).color(new RenderColor(argb)).next();
|
||||
mesh.quad(i1, i2, i3, i4);
|
||||
}
|
||||
|
||||
private static void addCube(MeshBuilder mesh, Vec3 pos, Vec3 rotation, float size, int diagonalArgb, int outlineArgb) {
|
||||
Quaternionf quaternion = new Quaternionf().rotationXYZ((float) rotation.x, (float) rotation.y, (float) rotation.z);
|
||||
Vector3f[] corners = new Vector3f[]{
|
||||
rotatedCorner(-0.5f, -0.5f, -0.5f, quaternion, size, pos),
|
||||
rotatedCorner(0.5f, -0.5f, -0.5f, quaternion, size, pos),
|
||||
rotatedCorner(0.5f, 0.5f, -0.5f, quaternion, size, pos),
|
||||
rotatedCorner(-0.5f, 0.5f, -0.5f, quaternion, size, pos),
|
||||
rotatedCorner(-0.5f, -0.5f, 0.5f, quaternion, size, pos),
|
||||
rotatedCorner(0.5f, -0.5f, 0.5f, quaternion, size, pos),
|
||||
rotatedCorner(0.5f, 0.5f, 0.5f, quaternion, size, pos),
|
||||
rotatedCorner(-0.5f, 0.5f, 0.5f, quaternion, size, pos)
|
||||
};
|
||||
|
||||
for (int[] edge : CUBE_EDGES) {
|
||||
addLine(mesh, corners[edge[0]], corners[edge[1]], outlineArgb);
|
||||
}
|
||||
for (int[] diagonal : BODY_DIAGONALS) {
|
||||
addLine(mesh, corners[diagonal[0]], corners[diagonal[1]], diagonalArgb);
|
||||
}
|
||||
}
|
||||
|
||||
private static Vector3f rotatedCorner(float x, float y, float z, Quaternionf quaternion, float size, Vec3 pos) {
|
||||
return new Vector3f(x, y, z)
|
||||
.rotate(quaternion)
|
||||
.mul(size)
|
||||
.add((float) pos.x, (float) pos.y, (float) pos.z);
|
||||
}
|
||||
|
||||
private static void addLine(MeshBuilder mesh, Vector3f from, Vector3f to, int argb) {
|
||||
addLineGradient(mesh, from, to, argb, argb);
|
||||
}
|
||||
|
||||
private static void addLineGradient(MeshBuilder mesh, Vector3f from, Vector3f to, int fromArgb, int toArgb) {
|
||||
mesh.ensureLineCapacity();
|
||||
int i1 = mesh.vec3(from.x(), from.y(), from.z()).color(new RenderColor(fromArgb)).next();
|
||||
int i2 = mesh.vec3(to.x(), to.y(), to.z()).color(new RenderColor(toArgb)).next();
|
||||
mesh.line(i1, i2);
|
||||
}
|
||||
|
||||
private 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);
|
||||
}
|
||||
|
||||
private static Vector3f sphereNormal(Quaternionf rotation, 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).normalize();
|
||||
}
|
||||
|
||||
private 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 rowSize = BUBBLE_SPHERE_SLICES + 1;
|
||||
int baseVertex = mesh.getVertexCount();
|
||||
mesh.ensureCapacity((BUBBLE_SPHERE_STACKS + 1) * rowSize, BUBBLE_SPHERE_STACKS * BUBBLE_SPHERE_SLICES * 6);
|
||||
|
||||
for (int stack = 0; stack <= BUBBLE_SPHERE_STACKS; stack++) {
|
||||
float theta = (float) Math.PI * stack / (float) BUBBLE_SPHERE_STACKS;
|
||||
for (int slice = 0; slice <= BUBBLE_SPHERE_SLICES; slice++) {
|
||||
float phi = (float) (Math.PI * 2.0) * slice / (float) BUBBLE_SPHERE_SLICES;
|
||||
Vector3f point = spherePoint(center, rotation, radius, theta, phi);
|
||||
int vertexColor = sphereSurfaceColor(baseColor, brightColor, sphereNormal(rotation, theta, phi), light, alpha);
|
||||
mesh.vec3(point.x(), point.y(), point.z()).color(new RenderColor(vertexColor)).next();
|
||||
}
|
||||
}
|
||||
|
||||
for (int stack = 0; stack < BUBBLE_SPHERE_STACKS; stack++) {
|
||||
int row = baseVertex + stack * rowSize;
|
||||
int nextRow = row + rowSize;
|
||||
for (int slice = 0; slice < BUBBLE_SPHERE_SLICES; slice++) {
|
||||
int a = row + slice;
|
||||
int b = nextRow + slice;
|
||||
int c = nextRow + slice + 1;
|
||||
int d = row + slice + 1;
|
||||
mesh.triangle(a, b, c);
|
||||
mesh.triangle(c, d, a);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int sphereSurfaceColor(int baseColor, int brightColor, Vector3f normal, Vector3f light, float alpha) {
|
||||
float lit = Mth.clamp(normal.dot(light) * 0.5f + 0.5f, 0.0f, 1.0f);
|
||||
float rim = 1.0f - Math.abs(normal.z());
|
||||
float vertical = Mth.clamp(normal.y() * 0.5f + 0.5f, 0.0f, 1.0f);
|
||||
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);
|
||||
|
|
@ -472,14 +294,14 @@ public class WorldParticles extends Module {
|
|||
|
||||
if (spriteMesh != null) {
|
||||
int spriteArgb = multiplyAlpha(baseColor, alpha * SPRITE_ALPHA_MULTIPLIER);
|
||||
addBillboardQuad(spriteMesh, pos.x, pos.y, pos.z, particle.size * SPRITE_SIZE_MULTIPLIER, camRot, spriteArgb);
|
||||
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);
|
||||
addCube(lineMesh, pos, rotation, particle.size, diagonalArgb, outlineArgb);
|
||||
WorldParticlesGeometry.addCube(lineMesh, pos, rotation, particle.size, diagonalArgb, outlineArgb);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -531,7 +353,7 @@ public class WorldParticles extends Module {
|
|||
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));
|
||||
addBillboardQuad(
|
||||
WorldParticlesGeometry.addBillboardQuad(
|
||||
distortionMesh,
|
||||
pos.x, pos.y, pos.z,
|
||||
particle.size * FUNNEL_DISTORTION_OUTER_SIZE_MULTIPLIER * scale * (1.0f + rimPulse * 0.055f),
|
||||
|
|
@ -542,7 +364,7 @@ public class WorldParticles extends Module {
|
|||
|
||||
float distortionRoll = particle.visualPhase + ageSeconds * particle.distortionSpin;
|
||||
int distortionArgb = funnelColor(baseColor, 0xFFFFFF, alpha * FUNNEL_DISTORTION_ALPHA_MULTIPLIER);
|
||||
addBillboardQuad(
|
||||
WorldParticlesGeometry.addBillboardQuad(
|
||||
distortionMesh,
|
||||
pos.x, pos.y, pos.z,
|
||||
particle.size * FUNNEL_DISTORTION_SIZE_MULTIPLIER * scale,
|
||||
|
|
@ -553,7 +375,7 @@ public class WorldParticles extends Module {
|
|||
|
||||
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));
|
||||
addBillboardQuad(
|
||||
WorldParticlesGeometry.addBillboardQuad(
|
||||
distortionMesh,
|
||||
pos.x, pos.y, pos.z,
|
||||
particle.size * FUNNEL_DISTORTION_INNER_SIZE_MULTIPLIER * scale,
|
||||
|
|
@ -566,7 +388,7 @@ public class WorldParticles extends Module {
|
|||
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));
|
||||
addBillboardQuad(
|
||||
WorldParticlesGeometry.addBillboardQuad(
|
||||
eyeMesh,
|
||||
pos.x, pos.y, pos.z,
|
||||
particle.size * (FUNNEL_EYE_SIZE_MULTIPLIER + 0.48f) * scale * (1.0f + rimPulse * 0.045f),
|
||||
|
|
@ -576,7 +398,7 @@ public class WorldParticles extends Module {
|
|||
);
|
||||
|
||||
int ghostA = funnelColor(baseColor, 0x92E8FF, alpha * FUNNEL_GHOST_ALPHA_MULTIPLIER * (0.70f + rimPulse * 0.16f));
|
||||
addBillboardQuad(
|
||||
WorldParticlesGeometry.addBillboardQuad(
|
||||
eyeMesh,
|
||||
pos.x, pos.y, pos.z,
|
||||
particle.size * 3.74f * scale,
|
||||
|
|
@ -586,7 +408,7 @@ public class WorldParticles extends Module {
|
|||
);
|
||||
|
||||
int ghostB = funnelColor(baseColor, 0xFF9D47, alpha * FUNNEL_GHOST_ALPHA_MULTIPLIER * 0.68f * (1.0f - rimPulse * 0.18f));
|
||||
addBillboardQuad(
|
||||
WorldParticlesGeometry.addBillboardQuad(
|
||||
eyeMesh,
|
||||
pos.x, pos.y, pos.z,
|
||||
particle.size * 4.88f * scale,
|
||||
|
|
@ -596,7 +418,7 @@ public class WorldParticles extends Module {
|
|||
);
|
||||
|
||||
int eyeArgb = funnelColor(baseColor, 0xFFFFFF, alpha * FUNNEL_EYE_ALPHA_MULTIPLIER);
|
||||
addBillboardQuad(
|
||||
WorldParticlesGeometry.addBillboardQuad(
|
||||
eyeMesh,
|
||||
pos.x, pos.y, pos.z,
|
||||
particle.size * FUNNEL_EYE_SIZE_MULTIPLIER * scale,
|
||||
|
|
@ -614,7 +436,7 @@ public class WorldParticles extends Module {
|
|||
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);
|
||||
addBillboardQuad(
|
||||
WorldParticlesGeometry.addBillboardQuad(
|
||||
bloomMesh,
|
||||
pos.x, pos.y, pos.z,
|
||||
particle.size * (1.45f + flash * 0.82f) * scale,
|
||||
|
|
@ -651,11 +473,11 @@ public class WorldParticles extends Module {
|
|||
float sphereRadius = radius * 0.82f;
|
||||
|
||||
if (shellMesh != null) {
|
||||
addSphereShell(shellMesh, pos, sphereRotation, sphereRadius, baseColor, brightColor, alpha);
|
||||
WorldParticlesSphere.addSphereShell(shellMesh, pos, sphereRotation, sphereRadius, baseColor, brightColor, alpha);
|
||||
}
|
||||
|
||||
if (spriteMesh != null) {
|
||||
addBillboardQuadGradient(
|
||||
WorldParticlesGeometry.addBillboardQuadGradient(
|
||||
spriteMesh,
|
||||
pos.x, pos.y, pos.z,
|
||||
particle.size * BUBBLE_GLOW_SIZE_MULTIPLIER * pop,
|
||||
|
|
@ -669,7 +491,7 @@ public class WorldParticles extends Module {
|
|||
|
||||
Vec3 depth = offsetInBillboardPlane(pos, camRot, depthAngle, radius * 0.22f);
|
||||
float depthAlpha = alpha * BUBBLE_DEPTH_ALPHA_MULTIPLIER * (0.78f + pulse * 0.18f);
|
||||
addBillboardQuadGradient(
|
||||
WorldParticlesGeometry.addBillboardQuadGradient(
|
||||
spriteMesh,
|
||||
depth.x, depth.y, depth.z,
|
||||
radius * 0.58f,
|
||||
|
|
@ -682,7 +504,7 @@ public class WorldParticles extends Module {
|
|||
);
|
||||
|
||||
float fillAlpha = alpha * BUBBLE_FILL_ALPHA_MULTIPLIER;
|
||||
addBillboardQuadGradient(
|
||||
WorldParticlesGeometry.addBillboardQuadGradient(
|
||||
spriteMesh,
|
||||
pos.x, pos.y, pos.z,
|
||||
radius,
|
||||
|
|
@ -696,7 +518,7 @@ public class WorldParticles extends Module {
|
|||
|
||||
Vec3 highlight = offsetInBillboardPlane(pos, camRot, lightAngle, radius * 0.42f);
|
||||
float highlightAlpha = alpha * BUBBLE_HIGHLIGHT_ALPHA_MULTIPLIER * (1.0f - lifeT * 0.22f);
|
||||
addBillboardQuadGradient(
|
||||
WorldParticlesGeometry.addBillboardQuadGradient(
|
||||
spriteMesh,
|
||||
highlight.x, highlight.y, highlight.z,
|
||||
particle.size * BUBBLE_HIGHLIGHT_SIZE_MULTIPLIER * (0.74f + pulse * 0.22f),
|
||||
|
|
@ -710,7 +532,7 @@ public class WorldParticles extends Module {
|
|||
|
||||
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);
|
||||
addBillboardQuadGradient(
|
||||
WorldParticlesGeometry.addBillboardQuadGradient(
|
||||
spriteMesh,
|
||||
secondaryHighlight.x, secondaryHighlight.y, secondaryHighlight.z,
|
||||
particle.size * BUBBLE_SECONDARY_HIGHLIGHT_SIZE_MULTIPLIER,
|
||||
|
|
@ -808,7 +630,7 @@ public class WorldParticles extends Module {
|
|||
default -> 0x55F7FF;
|
||||
};
|
||||
|
||||
addBillboardRect(
|
||||
WorldParticlesGeometry.addBillboardRect(
|
||||
mesh,
|
||||
leakPos.x, leakPos.y, leakPos.z,
|
||||
particle.size * scale * (0.055f + seed * 0.035f),
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.effects.ambient.geometry;
|
||||
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.joml.Quaternionf;
|
||||
import org.joml.Vector3f;
|
||||
import dev.loki.lovisual.render.engine.color.RenderColor;
|
||||
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
|
||||
|
||||
/**
|
||||
* Геометрия WorldParticles: билдборды, кубы, линии, сферы.
|
||||
*/
|
||||
public final class WorldParticlesGeometry {
|
||||
private WorldParticlesGeometry() {}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public static void addBillboardQuad(MeshBuilder mesh, double cx, double cy, double cz, float size, Quaternionf camRot, float rollRadians, int argb) {
|
||||
addBillboardQuadGradient(mesh, cx, cy, cz, size, camRot, rollRadians, argb, argb, argb, argb);
|
||||
}
|
||||
|
||||
public static void addBillboardQuadGradient(MeshBuilder mesh, double cx, double cy, double cz, float size, Quaternionf camRot, float rollRadians, int bottomLeftArgb, int bottomRightArgb, int topRightArgb, int topLeftArgb) {
|
||||
Vector3f baseRight = new Vector3f(1.0f, 0.0f, 0.0f).rotate(camRot);
|
||||
Vector3f baseUp = new Vector3f(0.0f, 1.0f, 0.0f).rotate(camRot);
|
||||
float cos = (float) Math.cos(rollRadians);
|
||||
float sin = (float) Math.sin(rollRadians);
|
||||
Vector3f right = new Vector3f(baseRight).mul(cos).add(new Vector3f(baseUp).mul(sin)).mul(size);
|
||||
Vector3f up = new Vector3f(baseUp).mul(cos).sub(new Vector3f(baseRight).mul(sin)).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.0, 1.0).color(new RenderColor(bottomLeftArgb)).next();
|
||||
int i2 = mesh.vec3(p2x, p2y, p2z).vec2(1.0, 1.0).color(new RenderColor(bottomRightArgb)).next();
|
||||
int i3 = mesh.vec3(p3x, p3y, p3z).vec2(1.0, 0.0).color(new RenderColor(topRightArgb)).next();
|
||||
int i4 = mesh.vec3(p4x, p4y, p4z).vec2(0.0, 0.0).color(new RenderColor(topLeftArgb)).next();
|
||||
mesh.quad(i1, i2, i3, i4);
|
||||
}
|
||||
|
||||
public static void addBillboardRect(MeshBuilder mesh, double cx, double cy, double cz, float halfWidth, float halfHeight, Quaternionf camRot, float rollRadians, int argb) {
|
||||
Vector3f baseRight = new Vector3f(1.0f, 0.0f, 0.0f).rotate(camRot);
|
||||
Vector3f baseUp = new Vector3f(0.0f, 1.0f, 0.0f).rotate(camRot);
|
||||
float cos = (float) Math.cos(rollRadians);
|
||||
float sin = (float) Math.sin(rollRadians);
|
||||
Vector3f right = new Vector3f(baseRight).mul(cos).add(new Vector3f(baseUp).mul(sin)).mul(halfWidth);
|
||||
Vector3f up = new Vector3f(baseUp).mul(cos).sub(new Vector3f(baseRight).mul(sin)).mul(halfHeight);
|
||||
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.0, 1.0).color(new RenderColor(argb)).next();
|
||||
int i2 = mesh.vec3(p2x, p2y, p2z).vec2(1.0, 1.0).color(new RenderColor(argb)).next();
|
||||
int i3 = mesh.vec3(p3x, p3y, p3z).vec2(1.0, 0.0).color(new RenderColor(argb)).next();
|
||||
int i4 = mesh.vec3(p4x, p4y, p4z).vec2(0.0, 0.0).color(new RenderColor(argb)).next();
|
||||
mesh.quad(i1, i2, i3, i4);
|
||||
}
|
||||
|
||||
public static void addCube(MeshBuilder mesh, Vec3 pos, Vec3 rotation, float size, int diagonalArgb, int outlineArgb) {
|
||||
Quaternionf q = new Quaternionf().rotationXYZ((float) rotation.x, (float) rotation.y, (float) rotation.z);
|
||||
Vector3f[] corners = {
|
||||
rotatedCorner(-0.5f, -0.5f, -0.5f, q, size, pos),
|
||||
rotatedCorner(0.5f, -0.5f, -0.5f, q, size, pos),
|
||||
rotatedCorner(0.5f, 0.5f, -0.5f, q, size, pos),
|
||||
rotatedCorner(-0.5f, 0.5f, -0.5f, q, size, pos),
|
||||
rotatedCorner(-0.5f, -0.5f, 0.5f, q, size, pos),
|
||||
rotatedCorner(0.5f, -0.5f, 0.5f, q, size, pos),
|
||||
rotatedCorner(0.5f, 0.5f, 0.5f, q, size, pos),
|
||||
rotatedCorner(-0.5f, 0.5f, 0.5f, q, size, pos)
|
||||
};
|
||||
int[][] 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}};
|
||||
int[][] diags = {{0,6},{1,7},{2,4},{3,5}};
|
||||
for (int[] e : edges) addLine(mesh, corners[e[0]], corners[e[1]], outlineArgb);
|
||||
for (int[] d : diags) addLine(mesh, corners[d[0]], corners[d[1]], diagonalArgb);
|
||||
}
|
||||
|
||||
private static Vector3f rotatedCorner(float x, float y, float z, Quaternionf q, float size, Vec3 pos) {
|
||||
return new Vector3f(x, y, z).rotate(q).mul(size).add((float) pos.x, (float) pos.y, (float) pos.z);
|
||||
}
|
||||
|
||||
public static void addLine(MeshBuilder mesh, Vector3f from, Vector3f to, int argb) {
|
||||
addLineGradient(mesh, from, to, argb, argb);
|
||||
}
|
||||
|
||||
public static void addLineGradient(MeshBuilder mesh, Vector3f from, Vector3f to, int fromArgb, int toArgb) {
|
||||
mesh.ensureLineCapacity();
|
||||
int i1 = mesh.vec3(from.x(), from.y(), from.z()).color(new RenderColor(fromArgb)).next();
|
||||
int i2 = mesh.vec3(to.x(), to.y(), to.z()).color(new RenderColor(toArgb)).next();
|
||||
mesh.line(i1, i2);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
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;
|
||||
import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors;
|
||||
import dev.loki.lovisual.render.engine.color.RenderColor;
|
||||
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
|
||||
|
||||
/**
|
||||
* Сферы WorldParticles (пузыри).
|
||||
*/
|
||||
public final class WorldParticlesSphere {
|
||||
private WorldParticlesSphere() {}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public static Vector3f sphereNormal(Quaternionf rotation, 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).normalize();
|
||||
}
|
||||
|
||||
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 rowSize = slices + 1;
|
||||
int baseVertex = mesh.getVertexCount();
|
||||
mesh.ensureCapacity((stacks + 1) * rowSize, stacks * slices * 6);
|
||||
for (int stack = 0; stack <= stacks; stack++) {
|
||||
float theta = (float) Math.PI * stack / (float) stacks;
|
||||
for (int slice = 0; slice <= slices; slice++) {
|
||||
float phi = (float) (Math.PI * 2.0) * slice / (float) slices;
|
||||
Vector3f point = spherePoint(center, rotation, radius, theta, phi);
|
||||
int vertexColor = sphereSurfaceColor(baseColor, brightColor, sphereNormal(rotation, theta, phi), light, alpha);
|
||||
mesh.vec3(point.x(), point.y(), point.z()).color(new RenderColor(vertexColor)).next();
|
||||
}
|
||||
}
|
||||
for (int stack = 0; stack < stacks; stack++) {
|
||||
int row = baseVertex + stack * rowSize;
|
||||
int nextRow = row + rowSize;
|
||||
for (int slice = 0; slice < slices; slice++) {
|
||||
int a = row + slice, b = nextRow + slice, c = nextRow + slice + 1, d = row + slice + 1;
|
||||
mesh.triangle(a, b, c); mesh.triangle(c, d, a);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int sphereSurfaceColor(int baseColor, int brightColor, Vector3f normal, Vector3f light, float alpha) {
|
||||
float lit = Mth.clamp(normal.dot(light) * 0.5f + 0.5f, 0.0f, 1.0f);
|
||||
float rim = 1.0f - Math.abs(normal.z());
|
||||
float vertical = Mth.clamp(normal.y() * 0.5f + 0.5f, 0.0f, 1.0f);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.module.modules.visuals.effects;
|
||||
package dev.loki.lovisual.features.module.modules.visuals.effects.hits;
|
||||
|
||||
import com.mojang.blaze3d.pipeline.RenderPipeline;
|
||||
import dev.loki.lovisual.config.values.color.RGBAColorValue;
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.module.modules.visuals.effects;
|
||||
package dev.loki.lovisual.features.module.modules.visuals.effects.hits;
|
||||
|
||||
import dev.loki.lovisual.util.time.GameClock;
|
||||
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.module.modules.visuals.effects;
|
||||
package dev.loki.lovisual.features.module.modules.visuals.effects.hits;
|
||||
|
||||
import dev.loki.lovisual.util.time.GameClock;
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ public final class BlockESP extends Module {
|
|||
num("range", 64, 8, 160);
|
||||
|
||||
private final NumberValue<Float> scanInterval =
|
||||
num("scan_interval", 2.0f, 0.5f, 10.0f);
|
||||
num("scan_interval", 1.0f, 0.5f, 10.0f);
|
||||
|
||||
private final RGBAColorValue fillColor =
|
||||
color("fill_color", "#4000FFC8");
|
||||
|
|
@ -65,6 +65,7 @@ public final class BlockESP extends Module {
|
|||
private final AtomicBoolean scanning = new AtomicBoolean(false);
|
||||
private final AtomicBoolean scanInProgress = new AtomicBoolean(false);
|
||||
private volatile List<BlockPos> foundBlocks = new ArrayList<>();
|
||||
private int nearbyTickCounter = 0;
|
||||
|
||||
public BlockESP() {
|
||||
}
|
||||
|
|
@ -90,6 +91,10 @@ public final class BlockESP extends Module {
|
|||
scanTimer.reset();
|
||||
requestScan();
|
||||
}
|
||||
// Быстрое обновление рядом (камень→спавнер при подходе) — 4 тика ≈0.2с, радиус 16
|
||||
if (++nearbyTickCounter % 4 == 0) {
|
||||
updateNearby();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -181,6 +186,45 @@ public final class BlockESP extends Module {
|
|||
foundBlocks = result;
|
||||
}
|
||||
|
||||
private void updateNearby() {
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
if (mc.level == null || mc.player == null) return;
|
||||
if (blockList.get().isEmpty()) return;
|
||||
BlockPos center = mc.player.blockPosition();
|
||||
int nearbyR = Math.min(16, range.get());
|
||||
int minX = center.getX() - nearbyR;
|
||||
int maxX = center.getX() + nearbyR;
|
||||
int minY = Math.max(mc.level.getMinY(), center.getY() - nearbyR);
|
||||
int maxY = Math.min(mc.level.getMaxY(), center.getY() + nearbyR);
|
||||
int minZ = center.getZ() - nearbyR;
|
||||
int maxZ = center.getZ() + nearbyR;
|
||||
// snapshot для быстрой проверки наличия
|
||||
List<BlockPos> snapshot = foundBlocks;
|
||||
java.util.HashSet<BlockPos> set = new java.util.HashSet<>(snapshot);
|
||||
boolean added = false;
|
||||
List<BlockPos> toAdd = new ArrayList<>();
|
||||
for (int x = minX; x <= maxX; x++) {
|
||||
for (int y = minY; y <= maxY; y++) {
|
||||
for (int z = minZ; z <= maxZ; z++) {
|
||||
BlockPos pos = new BlockPos(x, y, z);
|
||||
if (set.contains(pos)) continue;
|
||||
BlockState state = mc.level.getBlockState(pos);
|
||||
if (state.isAir()) continue;
|
||||
if (blockList.get().contains(blockId(state.getBlock()))) {
|
||||
toAdd.add(pos.immutable());
|
||||
added = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (added) {
|
||||
List<BlockPos> updated = new ArrayList<>(snapshot.size() + toAdd.size());
|
||||
updated.addAll(snapshot);
|
||||
updated.addAll(toAdd);
|
||||
foundBlocks = updated;
|
||||
}
|
||||
}
|
||||
|
||||
private String blockId(Block block) {
|
||||
return BuiltInRegistries.BLOCK.getKey(block).toString();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.module.modules.visuals.items;
|
||||
package dev.loki.lovisual.features.module.modules.visuals.items.held;
|
||||
|
||||
import dev.loki.lovisual.config.values.primitive.BooleanValue;
|
||||
import dev.loki.lovisual.config.values.mode.ModeValue;
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.module.modules.visuals.items;
|
||||
package dev.loki.lovisual.features.module.modules.visuals.items.held;
|
||||
|
||||
import dev.loki.lovisual.config.values.primitive.BooleanValue;
|
||||
import dev.loki.lovisual.config.values.primitive.NumberValue;
|
||||
|
|
@ -15,7 +15,7 @@ import dev.loki.lovisual.features.module.core.ModuleInfo;
|
|||
import dev.loki.lovisual.util.animation.EaseCurves;
|
||||
import dev.loki.lovisual.util.animation.hand.state.HandAnimState;
|
||||
import dev.loki.lovisual.render.helpers.util.TickDelta;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.scene.Chams;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.scene.visual.Chams;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.player.LocalPlayer;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
|
|
@ -10,7 +10,7 @@ package dev.loki.lovisual.features.module.modules.visuals.optimization;
|
|||
import net.minecraft.core.particles.ParticleTypes;
|
||||
import dev.loki.lovisual.config.values.primitive.BooleanValue;
|
||||
import dev.loki.lovisual.config.values.primitive.NumberValue;
|
||||
import dev.loki.lovisual.events.impl.render.ParticleSpawnEvent;
|
||||
import dev.loki.lovisual.events.impl.render.particles.ParticleSpawnEvent;
|
||||
import dev.loki.lovisual.events.meta.EventHandler;
|
||||
import dev.loki.lovisual.features.module.core.Module;
|
||||
import dev.loki.lovisual.features.module.core.ModuleCategory;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.module.modules.visuals.scene;
|
||||
package dev.loki.lovisual.features.module.modules.visuals.scene.visual;
|
||||
|
||||
import dev.loki.lovisual.features.theme.Theme;
|
||||
import com.mojang.blaze3d.GpuFormat;
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.module.modules.visuals.scene;
|
||||
package dev.loki.lovisual.features.module.modules.visuals.scene.visual;
|
||||
|
||||
import dev.loki.lovisual.features.theme.Theme;
|
||||
import com.mojang.blaze3d.textures.GpuTextureView;
|
||||
|
|
@ -19,7 +19,7 @@ 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.features.module.modules.visuals.scene.ReimaginedVisual;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.scene.visual.ReimaginedVisual;
|
||||
import dev.loki.lovisual.features.module.phase.WorldPhase;
|
||||
import dev.loki.lovisual.features.module.core.Module;
|
||||
import dev.loki.lovisual.render.engine.postprocess.blur.DepthOfFieldDepthSource;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.weather;
|
||||
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;
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.module.modules.visuals.weather;
|
||||
package dev.loki.lovisual.features.module.modules.visuals.weather.precip;
|
||||
|
||||
import dev.loki.lovisual.config.values.mode.EnumValue;
|
||||
import dev.loki.lovisual.config.values.primitive.BooleanValue;
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.features.module.modules.visuals.weather;
|
||||
package dev.loki.lovisual.features.module.modules.visuals.weather.sky;
|
||||
|
||||
import dev.loki.lovisual.config.values.primitive.NumberValue;
|
||||
import dev.loki.lovisual.config.values.color.RGBColorValue;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dev.loki.lovisual.features.module.modules.visuals.weather;
|
||||
package dev.loki.lovisual.features.module.modules.visuals.weather.sky;
|
||||
|
||||
import dev.loki.lovisual.config.values.mode.ModeValue;
|
||||
import dev.loki.lovisual.config.values.primitive.BooleanValue;
|
||||
|
|
@ -37,507 +37,57 @@ public enum Themes {
|
|||
public static final String THEME_PRISM = "prism";
|
||||
public static final String THEME_CINDER = "cinder";
|
||||
public static final String THEME_FOREST = "forest";
|
||||
private static final Theme CLASSIC = new Theme(
|
||||
0xF012191B, // window bg (cooler, unchanged)
|
||||
0xF0142226, // header darker
|
||||
0x60203030, // window stroke
|
||||
0x16202523, // surface for controls
|
||||
0x28353A3E, // surface hover
|
||||
0x5043A7C8, // card enabled overlay
|
||||
0x334046, // card disabled
|
||||
0xFFFFFFFF, // text primary
|
||||
0x88C6D2CD, // text muted
|
||||
0xFF5CC8E7, // accent
|
||||
0x805CC8E7, // accent soft
|
||||
0x60FFFFFF // stroke soft
|
||||
);
|
||||
private static final Theme LEGACY = new Theme(
|
||||
0xF0121314, // window bg (old fixed style)
|
||||
0xF0000205, // header
|
||||
0xE1121314, // stroke
|
||||
0xFF171819, // surface
|
||||
0xFF131315, // surface hover
|
||||
0x503F464D, // card enabled overlay
|
||||
0x33191C20, // card disabled
|
||||
0xFFF5F5FF, // text primary
|
||||
0x88A5A5A5, // text muted
|
||||
0xFF767C94, // accent
|
||||
0x80767C94, // accent soft
|
||||
0x60373746 // stroke soft
|
||||
);
|
||||
private static final Theme AMBER = new Theme(
|
||||
0xF019120D, // window bg (warm)
|
||||
0xF0201711, // header
|
||||
0x60C3652C, // stroke
|
||||
0xFF221711, // surface
|
||||
0xFF2B1E16, // hover
|
||||
0x50E08C3A, // card enabled overlay
|
||||
0x40302620, // card disabled
|
||||
0xFFFFF7EB, // text primary
|
||||
0x88E3C9A7, // text muted
|
||||
0xFFE69A43, // accent
|
||||
0x80E69A43, // accent soft
|
||||
0x60F5D7B0 // stroke soft
|
||||
);
|
||||
private static final Theme NOIR = new Theme(
|
||||
0xF00C0D10, // window bg (deep charcoal)
|
||||
0xF0121317, // header
|
||||
0x60393F4A, // stroke
|
||||
0xFF14161B, // surface
|
||||
0xFF1C1F26, // hover
|
||||
0x5039424F, // card enabled overlay
|
||||
0x40282D35, // card disabled
|
||||
0xFFF4F4F4, // text primary
|
||||
0x88C7CBD3, // text muted
|
||||
0xFFE16B78, // accent
|
||||
0x80E16B78, // accent soft
|
||||
0x605A6370 // stroke soft
|
||||
);
|
||||
private static final Theme PURPLE = new Theme(
|
||||
0xF0120E1A, // window bg (purple)
|
||||
0xF0181323, // header
|
||||
0x60433A5A, // stroke
|
||||
0xFF1C1627, // surface
|
||||
0xFF261D35, // hover
|
||||
0x505E4AA0, // card enabled overlay
|
||||
0x40312644, // card disabled
|
||||
0xFFF5F2FF, // text primary
|
||||
0x88CFC6E6, // text muted
|
||||
0xFF9B6BFF, // accent
|
||||
0x809B6BFF, // accent soft
|
||||
0x60705A9C // stroke soft
|
||||
);
|
||||
private static final Theme BLUE = new Theme(
|
||||
0xF0080F1E, // window bg (deep blue)
|
||||
0xF00D1626, // header
|
||||
0x60405A82, // stroke
|
||||
0xFF101B2B, // surface
|
||||
0xFF162338, // hover
|
||||
0x50406BB3, // card enabled overlay
|
||||
0x4023324A, // card disabled
|
||||
0xFFF0F5FF, // text primary
|
||||
0x88B2C4E6, // text muted
|
||||
0xFF2E64CC, // accent
|
||||
0x803A6FD6, // accent soft
|
||||
0x60465F8E // stroke soft
|
||||
);
|
||||
private static final Theme AZURE = new Theme(
|
||||
0xF00C1220, // window bg (blue)
|
||||
0xF0101A2A, // header
|
||||
0x60324B66, // stroke
|
||||
0xFF121C2C, // surface
|
||||
0xFF18253A, // hover
|
||||
0x503F6BAA, // card enabled overlay
|
||||
0x40283348, // card disabled
|
||||
0xFFF2F7FF, // text primary
|
||||
0x88BBD0EA, // text muted
|
||||
0xFF5DA7FF, // accent
|
||||
0x805DA7FF, // accent soft
|
||||
0x60507CA8 // stroke soft
|
||||
);
|
||||
private static final Theme NITRO = new Theme(
|
||||
0xFF121624, // window bg
|
||||
0xFF181E33, // header
|
||||
0x60465580, // stroke
|
||||
0xFF1A1F36, // surface
|
||||
0xFF232A45, // hover
|
||||
0x50A24CFF, // card enabled overlay
|
||||
0x40303A52, // card disabled
|
||||
0xFFF3F6FF, // text primary
|
||||
0x88C7D3F3, // text muted
|
||||
0xFF7A5CFF, // accent
|
||||
0x807A5CFF, // accent soft
|
||||
0x60829AC9 // stroke soft
|
||||
);
|
||||
private static final Theme SUNSET = new Theme(
|
||||
0xFF1A1116, // window bg
|
||||
0xFF211319, // header
|
||||
0x605C2B2B, // stroke
|
||||
0xFF24151C, // surface
|
||||
0xFF2E1A23, // hover
|
||||
0x50FF7A45, // card enabled overlay
|
||||
0x40261A1A, // card disabled
|
||||
0xFFFFF2E8, // text primary
|
||||
0x88E3B9A3, // text muted
|
||||
0xFFFF7A45, // accent
|
||||
0x80FF7A45, // accent soft
|
||||
0x60F5C1A6 // stroke soft
|
||||
);
|
||||
private static final Theme AURORA = new Theme(
|
||||
0xFF0D1A18, // window bg
|
||||
0xFF102320, // header
|
||||
0x602B5B57, // stroke
|
||||
0xFF132623, // surface
|
||||
0xFF1A312D, // hover
|
||||
0x5034D399, // card enabled overlay
|
||||
0x40304742, // card disabled
|
||||
0xFFF1FFF9, // text primary
|
||||
0x8897E3D0, // text muted
|
||||
0xFF2DD9C3, // accent
|
||||
0x802DD9C3, // accent soft
|
||||
0x6071B5A6 // stroke soft
|
||||
);
|
||||
private static final Theme MINT = new Theme(
|
||||
0xFF0E1917, // window bg
|
||||
0xFF112320, // header
|
||||
0x60255B53, // stroke
|
||||
0xFF132521, // surface
|
||||
0xFF1A2F2B, // hover
|
||||
0x5035CFA8, // card enabled overlay
|
||||
0x40304742, // card disabled
|
||||
0xFFF1FFF9, // text primary
|
||||
0x8897E3D0, // text muted
|
||||
0xFF43E0B2, // accent
|
||||
0x8043E0B2, // accent soft
|
||||
0x6071B5A6 // stroke soft
|
||||
);
|
||||
private static final Theme PEACH = new Theme(
|
||||
0xFF1A1210, // window bg
|
||||
0xFF221510, // header
|
||||
0x605C3A2A, // stroke
|
||||
0xFF241814, // surface
|
||||
0xFF2F211A, // hover
|
||||
0x50F2A269, // card enabled overlay
|
||||
0x40382A22, // card disabled
|
||||
0xFFFFF7F0, // text primary
|
||||
0x88E3C9A7, // text muted
|
||||
0xFFFFA46B, // accent
|
||||
0x80FFB680, // accent soft
|
||||
0x60F5D7B0 // stroke soft
|
||||
);
|
||||
private static final Theme LILAC = new Theme(
|
||||
0xFF14131E, // window bg
|
||||
0xFF1A1728, // header
|
||||
0x60433A5A, // stroke
|
||||
0xFF1B172A, // surface
|
||||
0xFF241D36, // hover
|
||||
0x506C5CBE, // card enabled overlay
|
||||
0x40312644, // card disabled
|
||||
0xFFF5F2FF, // text primary
|
||||
0x88CFC6E6, // text muted
|
||||
0xFFB79BFF, // accent
|
||||
0x80C8B0FF, // accent soft
|
||||
0x60705A9C // stroke soft
|
||||
);
|
||||
private static final Theme SAND = new Theme(
|
||||
0xFF181510, // window bg
|
||||
0xFF201A13, // header
|
||||
0x6050432F, // stroke
|
||||
0xFF221B14, // surface
|
||||
0xFF2C231B, // hover
|
||||
0x50E8C98C, // card enabled overlay
|
||||
0x40322A21, // card disabled
|
||||
0xFFFFF4E8, // text primary
|
||||
0x88D8C4A2, // text muted
|
||||
0xFFE8C98C, // accent
|
||||
0x80F0D8A8, // accent soft
|
||||
0x607A6A54 // stroke soft
|
||||
);
|
||||
private static final Theme DUSK = new Theme(
|
||||
0xFF141018, // window bg
|
||||
0xFF1B1422, // header
|
||||
0x60503A4A, // stroke
|
||||
0xFF1D1626, // surface
|
||||
0xFF271B34, // hover
|
||||
0x50FF875A, // card enabled overlay
|
||||
0x4030263B, // card disabled
|
||||
0xFFF6F1FF, // text primary
|
||||
0x88D6C9E6, // text muted
|
||||
0xFFFF8A4C, // accent
|
||||
0x80FF9B5A, // accent soft
|
||||
0x606B5A7A // stroke soft
|
||||
);
|
||||
private static final Theme PRISM = new Theme(
|
||||
0xFF0F1220, // window bg
|
||||
0xFF141A2B, // header
|
||||
0x60435173, // stroke
|
||||
0xFF151C2E, // surface
|
||||
0xFF1B243A, // hover
|
||||
0x504B7CFF, // card enabled overlay
|
||||
0x4031324A, // card disabled
|
||||
0xFFF3F6FF, // text primary
|
||||
0x88C2CDEB, // text muted
|
||||
0xFF5AC8FA, // accent
|
||||
0x8074B6FF, // accent soft
|
||||
0x606B7A9E // stroke soft
|
||||
);
|
||||
private static final Theme CINDER = new Theme(
|
||||
0xFF120E0F, // window bg
|
||||
0xFF181012, // header
|
||||
0x604A2F32, // stroke
|
||||
0xFF1A1214, // surface
|
||||
0xFF23171A, // hover
|
||||
0x50D24A4A, // card enabled overlay
|
||||
0x40281A1C, // card disabled
|
||||
0xFFFFF3F3, // text primary
|
||||
0x88E3B9B9, // text muted
|
||||
0xFFDC4A4A, // accent
|
||||
0x80E06A6A, // accent soft
|
||||
0x606B4E52 // stroke soft
|
||||
);
|
||||
private static final Theme FOREST = new Theme(
|
||||
0xFF101611, // window bg
|
||||
0xFF141E15, // header
|
||||
0x6043573D, // stroke
|
||||
0xFF161F18, // surface
|
||||
0xFF1D2B20, // hover
|
||||
0x5071C36C, // card enabled overlay
|
||||
0x40263225, // card disabled
|
||||
0xFFF3FFF1, // text primary
|
||||
0x88C5E3C1, // text muted
|
||||
0xFF7DD36B, // accent
|
||||
0x807DD36B, // accent soft
|
||||
0x60607B58 // stroke soft
|
||||
);
|
||||
private static final ThemeEntry CLASSIC_ENTRY = new ThemeEntry(
|
||||
THEME_CLASSIC,
|
||||
"Classic",
|
||||
true,
|
||||
CLASSIC,
|
||||
flatGradient(CLASSIC.windowBg()),
|
||||
subtleHeaderGradient(CLASSIC.windowHeader()),
|
||||
flatGradient(CLASSIC.surface()),
|
||||
flatGradient(CLASSIC.cardEnabled()),
|
||||
flatGradient(CLASSIC.windowStroke())
|
||||
);
|
||||
private static ThemeEntry currentEntry = CLASSIC_ENTRY;
|
||||
private static ThemeEntry transitionFromEntry = CLASSIC_ENTRY;
|
||||
private static ThemeEntry animatedEntryCache = CLASSIC_ENTRY;
|
||||
private static final ThemeEntry LEGACY_ENTRY = new ThemeEntry(
|
||||
THEME_LEGACY,
|
||||
"Legacy",
|
||||
true,
|
||||
LEGACY,
|
||||
flatGradient(LEGACY.windowBg()),
|
||||
subtleHeaderGradient(LEGACY.windowHeader()),
|
||||
flatGradient(LEGACY.surface()),
|
||||
flatGradient(LEGACY.cardEnabled()),
|
||||
flatGradient(LEGACY.windowStroke())
|
||||
);
|
||||
private static ThemeEntry currentEntry = ThemePresets1.CLASSIC_ENTRY;
|
||||
private static ThemeEntry transitionFromEntry = ThemePresets1.CLASSIC_ENTRY;
|
||||
private static ThemeEntry animatedEntryCache = ThemePresets1.CLASSIC_ENTRY;
|
||||
|
||||
private static final ThemeEntry AMBER_ENTRY = new ThemeEntry(
|
||||
THEME_AMBER,
|
||||
"Amber",
|
||||
true,
|
||||
AMBER,
|
||||
flatGradient(AMBER.windowBg()),
|
||||
subtleHeaderGradient(AMBER.windowHeader()),
|
||||
flatGradient(AMBER.surface()),
|
||||
flatGradient(AMBER.cardEnabled()),
|
||||
flatGradient(AMBER.windowStroke())
|
||||
);
|
||||
|
||||
private static final ThemeEntry NOIR_ENTRY = new ThemeEntry(
|
||||
THEME_NOIR,
|
||||
"Noir",
|
||||
true,
|
||||
NOIR,
|
||||
flatGradient(NOIR.windowBg()),
|
||||
subtleHeaderGradient(NOIR.windowHeader()),
|
||||
flatGradient(NOIR.surface()),
|
||||
flatGradient(NOIR.cardEnabled()),
|
||||
flatGradient(NOIR.windowStroke())
|
||||
);
|
||||
|
||||
private static final ThemeEntry PURPLE_ENTRY = new ThemeEntry(
|
||||
THEME_PURPLE,
|
||||
"Purple",
|
||||
true,
|
||||
PURPLE,
|
||||
flatGradient(PURPLE.windowBg()),
|
||||
subtleHeaderGradient(PURPLE.windowHeader()),
|
||||
flatGradient(PURPLE.surface()),
|
||||
flatGradient(PURPLE.cardEnabled()),
|
||||
flatGradient(PURPLE.windowStroke())
|
||||
);
|
||||
|
||||
private static final ThemeEntry BLUE_ENTRY = new ThemeEntry(
|
||||
THEME_BLUE,
|
||||
"Blue",
|
||||
true,
|
||||
BLUE,
|
||||
flatGradient(BLUE.windowBg()),
|
||||
subtleHeaderGradient(BLUE.windowHeader()),
|
||||
flatGradient(BLUE.surface()),
|
||||
flatGradient(BLUE.cardEnabled()),
|
||||
flatGradient(BLUE.windowStroke())
|
||||
);
|
||||
|
||||
private static final ThemeEntry AZURE_ENTRY = new ThemeEntry(
|
||||
THEME_AZURE,
|
||||
"Azure",
|
||||
true,
|
||||
AZURE,
|
||||
flatGradient(AZURE.windowBg()),
|
||||
subtleHeaderGradient(AZURE.windowHeader()),
|
||||
flatGradient(AZURE.surface()),
|
||||
flatGradient(AZURE.cardEnabled()),
|
||||
flatGradient(AZURE.windowStroke())
|
||||
);
|
||||
|
||||
private static final ThemeEntry NITRO_ENTRY = new ThemeEntry(
|
||||
THEME_NITRO,
|
||||
"Nitro",
|
||||
true,
|
||||
NITRO,
|
||||
new GradientSpec(true, 0xFF36205F, 0xFF0B7DFF, 45f),
|
||||
new GradientSpec(true, 0xFF4B2FAF, 0xFF24C1FF, 45f),
|
||||
new GradientSpec(true, 0xFF1A1F36, 0xFF121728, 90f),
|
||||
new GradientSpec(true, 0xFF5A37D5, 0xFF2AD1FF, 45f),
|
||||
new GradientSpec(true, 0xFF6C4BFF, 0xFF4BD0FF, 45f)
|
||||
);
|
||||
|
||||
private static final ThemeEntry SUNSET_ENTRY = new ThemeEntry(
|
||||
THEME_SUNSET,
|
||||
"Sunset",
|
||||
true,
|
||||
SUNSET,
|
||||
new GradientSpec(true, 0xFF2A1020, 0xFFB2431F, 45f),
|
||||
new GradientSpec(true, 0xFF3B1626, 0xFFC25B2A, 45f),
|
||||
new GradientSpec(true, 0xFF24151C, 0xFF1E1116, 90f),
|
||||
new GradientSpec(true, 0xFFFF8A4C, 0xFFFF4B7A, 45f),
|
||||
new GradientSpec(true, 0xFFFFB27A, 0xFFFF6A3A, 45f)
|
||||
);
|
||||
|
||||
private static final ThemeEntry AURORA_ENTRY = new ThemeEntry(
|
||||
THEME_AURORA,
|
||||
"Aurora",
|
||||
true,
|
||||
AURORA,
|
||||
new GradientSpec(true, 0xFF0B2A3C, 0xFF2A8E6B, 120f),
|
||||
new GradientSpec(true, 0xFF0E3B46, 0xFF1FB88B, 120f),
|
||||
new GradientSpec(true, 0xFF132623, 0xFF10201D, 90f),
|
||||
new GradientSpec(true, 0xFF2DD9C3, 0xFF5A7CFF, 135f),
|
||||
new GradientSpec(true, 0xFF78FFE5, 0xFF6FB1FF, 120f)
|
||||
);
|
||||
|
||||
private static final ThemeEntry MINT_ENTRY = new ThemeEntry(
|
||||
THEME_MINT,
|
||||
"Mint",
|
||||
true,
|
||||
MINT,
|
||||
new GradientSpec(true, 0xFF0B2A24, 0xFF1B3E36, 120f),
|
||||
new GradientSpec(true, 0xFF0F3A32, 0xFF1A6E59, 120f),
|
||||
new GradientSpec(true, 0xFF132521, 0xFF0F1E1B, 90f),
|
||||
new GradientSpec(true, 0xFF3DE3B4, 0xFFB7F2D9, 45f),
|
||||
new GradientSpec(true, 0xFF48E6B9, 0xFF7EF0D6, 120f)
|
||||
);
|
||||
|
||||
private static final ThemeEntry PEACH_ENTRY = new ThemeEntry(
|
||||
THEME_PEACH,
|
||||
"Peach",
|
||||
true,
|
||||
PEACH,
|
||||
new GradientSpec(true, 0xFF2A1512, 0xFF4B241A, 45f),
|
||||
new GradientSpec(true, 0xFF3B1B14, 0xFF6A301F, 45f),
|
||||
new GradientSpec(true, 0xFF241814, 0xFF1C1310, 90f),
|
||||
new GradientSpec(true, 0xFFFFB47A, 0xFFFF7FA8, 45f),
|
||||
new GradientSpec(true, 0xFFFFC28E, 0xFFFF8A5A, 45f)
|
||||
);
|
||||
|
||||
private static final ThemeEntry LILAC_ENTRY = new ThemeEntry(
|
||||
THEME_LILAC,
|
||||
"Lilac",
|
||||
true,
|
||||
LILAC,
|
||||
new GradientSpec(true, 0xFF241A3B, 0xFF1A3A55, 120f),
|
||||
new GradientSpec(true, 0xFF2F214A, 0xFF3D2F6A, 120f),
|
||||
new GradientSpec(true, 0xFF1B172A, 0xFF15121F, 90f),
|
||||
new GradientSpec(true, 0xFFB79BFF, 0xFF7AD9FF, 45f),
|
||||
new GradientSpec(true, 0xFFC5B0FF, 0xFF8BB2FF, 120f)
|
||||
);
|
||||
|
||||
private static final ThemeEntry SAND_ENTRY = new ThemeEntry(
|
||||
THEME_SAND,
|
||||
"Sand",
|
||||
true,
|
||||
SAND,
|
||||
new GradientSpec(true, 0xFF2A2016, 0xFF3A2F22, 45f),
|
||||
new GradientSpec(true, 0xFF32261B, 0xFF4A3A2B, 45f),
|
||||
new GradientSpec(true, 0xFF221B14, 0xFF1A1510, 90f),
|
||||
new GradientSpec(true, 0xFFF2D29B, 0xFFE8B67A, 45f),
|
||||
new GradientSpec(true, 0xFFF5E0B2, 0xFFD1A36C, 45f)
|
||||
);
|
||||
|
||||
private static final ThemeEntry DUSK_ENTRY = new ThemeEntry(
|
||||
THEME_DUSK,
|
||||
"Dusk",
|
||||
true,
|
||||
DUSK,
|
||||
new GradientSpec(true, 0xFF2B1630, 0xFF6B2A1F, 45f),
|
||||
new GradientSpec(true, 0xFF3B1B3A, 0xFF8A3B24, 45f),
|
||||
new GradientSpec(true, 0xFF1D1626, 0xFF141018, 90f),
|
||||
new GradientSpec(true, 0xFF7C4BFF, 0xFFFF9B3D, 45f),
|
||||
new GradientSpec(true, 0xFFA775FF, 0xFFFF7A3A, 45f)
|
||||
);
|
||||
|
||||
private static final ThemeEntry PRISM_ENTRY = new ThemeEntry(
|
||||
THEME_PRISM,
|
||||
"Prism",
|
||||
true,
|
||||
PRISM,
|
||||
new GradientSpec(true, 0xFF1A243A, 0xFF2A1E4A, 135f),
|
||||
new GradientSpec(true, 0xFF223050, 0xFF352060, 135f),
|
||||
new GradientSpec(true, 0xFF151C2E, 0xFF101522, 90f),
|
||||
new GradientSpec(true, 0xFF59C4FF, 0xFFB35CFF, 45f),
|
||||
new GradientSpec(true, 0xFF7AD4FF, 0xFFE36BFF, 45f)
|
||||
);
|
||||
|
||||
private static final ThemeEntry CINDER_ENTRY = new ThemeEntry(
|
||||
THEME_CINDER,
|
||||
"Cinder",
|
||||
true,
|
||||
CINDER,
|
||||
new GradientSpec(true, 0xFF1A0C0C, 0xFF2A1416, 45f),
|
||||
new GradientSpec(true, 0xFF241012, 0xFF3A191C, 45f),
|
||||
new GradientSpec(true, 0xFF1A1214, 0xFF120E0F, 90f),
|
||||
new GradientSpec(true, 0xFFDC4A4A, 0xFF401010, 45f),
|
||||
new GradientSpec(true, 0xFFE26A6A, 0xFF7A2A2A, 45f)
|
||||
);
|
||||
|
||||
private static final ThemeEntry FOREST_ENTRY = new ThemeEntry(
|
||||
THEME_FOREST,
|
||||
"Forest",
|
||||
true,
|
||||
FOREST,
|
||||
new GradientSpec(true, 0xFF1A241C, 0xFF2B3A24, 120f),
|
||||
new GradientSpec(true, 0xFF1F2B21, 0xFF2F4B2E, 120f),
|
||||
new GradientSpec(true, 0xFF161F18, 0xFF101611, 90f),
|
||||
new GradientSpec(true, 0xFF7DD36B, 0xFFC6E886, 45f),
|
||||
new GradientSpec(true, 0xFF9BE07C, 0xFF6BC26A, 45f)
|
||||
);
|
||||
|
||||
private static final Map<String, ThemeEntry> PRESET_ENTRIES = Map.ofEntries(
|
||||
Map.entry(THEME_CLASSIC, CLASSIC_ENTRY),
|
||||
Map.entry(THEME_LEGACY, LEGACY_ENTRY),
|
||||
Map.entry(THEME_AMBER, AMBER_ENTRY),
|
||||
Map.entry(THEME_NOIR, NOIR_ENTRY),
|
||||
Map.entry(THEME_PURPLE, PURPLE_ENTRY),
|
||||
Map.entry(THEME_BLUE, BLUE_ENTRY),
|
||||
Map.entry(THEME_AZURE, AZURE_ENTRY),
|
||||
Map.entry(THEME_NITRO, NITRO_ENTRY),
|
||||
Map.entry(THEME_SUNSET, SUNSET_ENTRY),
|
||||
Map.entry(THEME_AURORA, AURORA_ENTRY),
|
||||
Map.entry(THEME_MINT, MINT_ENTRY),
|
||||
Map.entry(THEME_PEACH, PEACH_ENTRY),
|
||||
Map.entry(THEME_LILAC, LILAC_ENTRY),
|
||||
Map.entry(THEME_SAND, SAND_ENTRY),
|
||||
Map.entry(THEME_DUSK, DUSK_ENTRY),
|
||||
Map.entry(THEME_PRISM, PRISM_ENTRY),
|
||||
Map.entry(THEME_CINDER, CINDER_ENTRY),
|
||||
Map.entry(THEME_FOREST, FOREST_ENTRY)
|
||||
Map.entry(THEME_CLASSIC, ThemePresets1.CLASSIC_ENTRY),
|
||||
Map.entry(THEME_LEGACY, ThemePresets1.LEGACY_ENTRY),
|
||||
Map.entry(THEME_AMBER, ThemePresets1.AMBER_ENTRY),
|
||||
Map.entry(THEME_NOIR, ThemePresets1.NOIR_ENTRY),
|
||||
Map.entry(THEME_PURPLE, ThemePresets1.PURPLE_ENTRY),
|
||||
Map.entry(THEME_BLUE, ThemePresets1.BLUE_ENTRY),
|
||||
Map.entry(THEME_AZURE, ThemePresets2.AZURE_ENTRY),
|
||||
Map.entry(THEME_NITRO, ThemePresets2.NITRO_ENTRY),
|
||||
Map.entry(THEME_SUNSET, ThemePresets2.SUNSET_ENTRY),
|
||||
Map.entry(THEME_AURORA, ThemePresets2.AURORA_ENTRY),
|
||||
Map.entry(THEME_MINT, ThemePresets2.MINT_ENTRY),
|
||||
Map.entry(THEME_PEACH, ThemePresets2.PEACH_ENTRY),
|
||||
Map.entry(THEME_LILAC, ThemePresets3.LILAC_ENTRY),
|
||||
Map.entry(THEME_SAND, ThemePresets3.SAND_ENTRY),
|
||||
Map.entry(THEME_DUSK, ThemePresets3.DUSK_ENTRY),
|
||||
Map.entry(THEME_PRISM, ThemePresets3.PRISM_ENTRY),
|
||||
Map.entry(THEME_CINDER, ThemePresets3.CINDER_ENTRY),
|
||||
Map.entry(THEME_FOREST, ThemePresets3.FOREST_ENTRY)
|
||||
);
|
||||
|
||||
private static final ThemeStore STORE = ThemeStore.get();
|
||||
|
||||
private static final long THEME_TRANSITION_MS = 340L;
|
||||
|
||||
private static Theme current = CLASSIC;
|
||||
private static Theme current = ThemePresets1.CLASSIC;
|
||||
private static String currentId = THEME_CLASSIC;
|
||||
private static Theme transitionFromTheme = CLASSIC;
|
||||
private static Theme transitionFromTheme = ThemePresets1.CLASSIC;
|
||||
private static long transitionStartMs = 0L;
|
||||
private static boolean transitionActive = false;
|
||||
private static Theme animatedThemeCache = CLASSIC;
|
||||
private static Theme animatedThemeCache = ThemePresets1.CLASSIC;
|
||||
private static int animatedCacheKey = -1;
|
||||
|
||||
static {
|
||||
|
|
@ -562,24 +112,24 @@ public enum Themes {
|
|||
|
||||
public static List<ThemeEntry> presetEntries() {
|
||||
List<ThemeEntry> out = new ArrayList<>();
|
||||
out.add(CLASSIC_ENTRY);
|
||||
out.add(LEGACY_ENTRY);
|
||||
out.add(AMBER_ENTRY);
|
||||
out.add(NOIR_ENTRY);
|
||||
out.add(PURPLE_ENTRY);
|
||||
out.add(BLUE_ENTRY);
|
||||
out.add(AZURE_ENTRY);
|
||||
out.add(NITRO_ENTRY);
|
||||
out.add(SUNSET_ENTRY);
|
||||
out.add(AURORA_ENTRY);
|
||||
out.add(MINT_ENTRY);
|
||||
out.add(PEACH_ENTRY);
|
||||
out.add(LILAC_ENTRY);
|
||||
out.add(SAND_ENTRY);
|
||||
out.add(DUSK_ENTRY);
|
||||
out.add(PRISM_ENTRY);
|
||||
out.add(CINDER_ENTRY);
|
||||
out.add(FOREST_ENTRY);
|
||||
out.add(ThemePresets1.CLASSIC_ENTRY);
|
||||
out.add(ThemePresets1.LEGACY_ENTRY);
|
||||
out.add(ThemePresets1.AMBER_ENTRY);
|
||||
out.add(ThemePresets1.NOIR_ENTRY);
|
||||
out.add(ThemePresets1.PURPLE_ENTRY);
|
||||
out.add(ThemePresets1.BLUE_ENTRY);
|
||||
out.add(ThemePresets2.AZURE_ENTRY);
|
||||
out.add(ThemePresets2.NITRO_ENTRY);
|
||||
out.add(ThemePresets2.SUNSET_ENTRY);
|
||||
out.add(ThemePresets2.AURORA_ENTRY);
|
||||
out.add(ThemePresets2.MINT_ENTRY);
|
||||
out.add(ThemePresets2.PEACH_ENTRY);
|
||||
out.add(ThemePresets3.LILAC_ENTRY);
|
||||
out.add(ThemePresets3.SAND_ENTRY);
|
||||
out.add(ThemePresets3.DUSK_ENTRY);
|
||||
out.add(ThemePresets3.PRISM_ENTRY);
|
||||
out.add(ThemePresets3.CINDER_ENTRY);
|
||||
out.add(ThemePresets3.FOREST_ENTRY);
|
||||
return Collections.unmodifiableList(out);
|
||||
}
|
||||
|
||||
|
|
@ -622,7 +172,7 @@ public enum Themes {
|
|||
|
||||
static ThemeEntry createCustomTheme(String name, ThemeEntry base, boolean select) {
|
||||
ThemeEntry src = base != null ? base : currentEntry();
|
||||
if (src == null) src = CLASSIC_ENTRY;
|
||||
if (src == null) src = ThemePresets1.CLASSIC_ENTRY;
|
||||
String displayName = name == null || name.isBlank() ? "Custom Theme" : name.trim();
|
||||
String id = STORE.uniqueCustomId(displayName);
|
||||
ThemeEntry entry = new ThemeEntry(
|
||||
|
|
@ -667,7 +217,7 @@ public enum Themes {
|
|||
public static ThemeEntry fromProfileValues(String id, String fallbackName, Map<String, Object> values) {
|
||||
String key = id == null || id.isBlank() ? uniqueCustomId(fallbackName) : id.toLowerCase();
|
||||
ThemeEntry existing = STORE.getById(key);
|
||||
ThemeEntry baseEntry = existing != null ? existing : CLASSIC_ENTRY;
|
||||
ThemeEntry baseEntry = existing != null ? existing : ThemePresets1.CLASSIC_ENTRY;
|
||||
Theme base = baseEntry.theme();
|
||||
Map<String, Object> map = values == null ? Map.of() : values;
|
||||
String name = stringValue(map.get("name"), fallbackName == null || fallbackName.isBlank() ? key : fallbackName);
|
||||
|
|
@ -704,7 +254,7 @@ public enum Themes {
|
|||
}
|
||||
|
||||
private static void writeGradient(Map<String, Object> out, String prefix, GradientSpec spec) {
|
||||
GradientSpec g = spec != null ? spec : flatGradient(CLASSIC.accent());
|
||||
GradientSpec g = spec != null ? spec : flatGradient(ThemePresets1.CLASSIC.accent());
|
||||
out.put(prefix + "GradientEnabled", g.enabled());
|
||||
out.put(prefix + "GradientStart", colorString(g.start()));
|
||||
out.put(prefix + "GradientEnd", colorString(g.end()));
|
||||
|
|
@ -755,14 +305,14 @@ public enum Themes {
|
|||
static void setTheme(String id) {
|
||||
if (id == null || id.isBlank()) {
|
||||
boolean animate = !THEME_CLASSIC.equals(currentId) || transitionActive;
|
||||
applyThemeFromEntry(CLASSIC_ENTRY, animate);
|
||||
applyThemeFromEntry(ThemePresets1.CLASSIC_ENTRY, animate);
|
||||
STORE.setCurrentId(THEME_CLASSIC);
|
||||
return;
|
||||
}
|
||||
String key = id.toLowerCase();
|
||||
ThemeEntry entry = STORE.getById(key);
|
||||
if (entry == null) entry = PRESET_ENTRIES.get(key);
|
||||
if (entry == null) entry = CLASSIC_ENTRY;
|
||||
if (entry == null) entry = ThemePresets1.CLASSIC_ENTRY;
|
||||
boolean animate = !entry.getId().equals(currentId) || transitionActive;
|
||||
applyThemeFromEntry(entry, animate);
|
||||
STORE.setCurrentId(entry.getId());
|
||||
|
|
@ -798,19 +348,19 @@ public enum Themes {
|
|||
}
|
||||
|
||||
static Theme classic() {
|
||||
return CLASSIC;
|
||||
return ThemePresets1.CLASSIC;
|
||||
}
|
||||
|
||||
private static void applyThemeFromStore() {
|
||||
String id = STORE.getCurrentId();
|
||||
ThemeEntry entry = STORE.getById(id);
|
||||
if (entry == null) entry = PRESET_ENTRIES.get(THEME_CLASSIC);
|
||||
if (entry == null) entry = CLASSIC_ENTRY;
|
||||
if (entry == null) entry = ThemePresets1.CLASSIC_ENTRY;
|
||||
applyThemeFromEntry(entry, false);
|
||||
}
|
||||
|
||||
private static void applyThemeFromEntry(ThemeEntry entry, boolean animate) {
|
||||
if (entry == null) entry = CLASSIC_ENTRY;
|
||||
if (entry == null) entry = ThemePresets1.CLASSIC_ENTRY;
|
||||
if (animate) {
|
||||
beginThemeTransition(displayedThemeRaw(), displayedEntryRaw());
|
||||
} else {
|
||||
|
|
@ -895,8 +445,8 @@ public enum Themes {
|
|||
}
|
||||
|
||||
private static Theme blendTheme(Theme from, Theme to, float t) {
|
||||
if (from == null) from = CLASSIC;
|
||||
if (to == null) to = CLASSIC;
|
||||
if (from == null) from = ThemePresets1.CLASSIC;
|
||||
if (to == null) to = ThemePresets1.CLASSIC;
|
||||
return new Theme(
|
||||
mixArgb(from.windowBg(), to.windowBg(), t),
|
||||
mixArgb(from.windowHeader(), to.windowHeader(), t),
|
||||
|
|
@ -914,8 +464,8 @@ public enum Themes {
|
|||
}
|
||||
|
||||
private static GradientSpec blendGradient(GradientSpec from, GradientSpec to, float t) {
|
||||
if (from == null) from = flatGradient(CLASSIC.accent());
|
||||
if (to == null) to = flatGradient(CLASSIC.accent());
|
||||
if (from == null) from = flatGradient(ThemePresets1.CLASSIC.accent());
|
||||
if (to == null) to = flatGradient(ThemePresets1.CLASSIC.accent());
|
||||
boolean enabled = from.enabled() || to.enabled();
|
||||
int start = mixArgb(from.start(), to.start(), t);
|
||||
int end = mixArgb(from.end(), to.end(), t);
|
||||
|
|
@ -947,11 +497,11 @@ public enum Themes {
|
|||
return out < 0.0f ? out + 360.0f : out;
|
||||
}
|
||||
|
||||
private static GradientSpec flatGradient(int color) {
|
||||
static GradientSpec flatGradient(int color) {
|
||||
return new GradientSpec(false, color, color, 90f);
|
||||
}
|
||||
|
||||
private static GradientSpec subtleHeaderGradient(int color) {
|
||||
static GradientSpec subtleHeaderGradient(int color) {
|
||||
int start = adjustBrightness(color, 0.08f);
|
||||
int end = adjustBrightness(color, -0.06f);
|
||||
return new GradientSpec(true, start, end, 90f);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,163 @@
|
|||
package dev.loki.lovisual.features.theme;
|
||||
|
||||
import dev.loki.lovisual.features.theme.Themes.Theme;
|
||||
import dev.loki.lovisual.features.theme.Themes.ThemeEntry;
|
||||
import dev.loki.lovisual.features.theme.Themes.GradientSpec;
|
||||
|
||||
/**
|
||||
* Пресет тем 1: CLASSIC, LEGACY, AMBER, NOIR, PURPLE, BLUE.
|
||||
*/
|
||||
final class ThemePresets1 {
|
||||
private ThemePresets1() {}
|
||||
|
||||
static final Theme CLASSIC = new Theme(
|
||||
0xF012191B, // window bg (cooler, unchanged)
|
||||
0xF0142226, // header darker
|
||||
0x60203030, // window stroke
|
||||
0x16202523, // surface for controls
|
||||
0x28353A3E, // surface hover
|
||||
0x5043A7C8, // card enabled overlay
|
||||
0x334046, // card disabled
|
||||
0xFFFFFFFF, // text primary
|
||||
0x88C6D2CD, // text muted
|
||||
0xFF5CC8E7, // accent
|
||||
0x805CC8E7, // accent soft
|
||||
0x60FFFFFF // stroke soft
|
||||
);
|
||||
static final Theme LEGACY = new Theme(
|
||||
0xF0121314, // window bg (old fixed style)
|
||||
0xF0000205, // header
|
||||
0xE1121314, // stroke
|
||||
0xFF171819, // surface
|
||||
0xFF131315, // surface hover
|
||||
0x503F464D, // card enabled overlay
|
||||
0x33191C20, // card disabled
|
||||
0xFFF5F5FF, // text primary
|
||||
0x88A5A5A5, // text muted
|
||||
0xFF767C94, // accent
|
||||
0x80767C94, // accent soft
|
||||
0x60373746 // stroke soft
|
||||
);
|
||||
static final Theme AMBER = new Theme(
|
||||
0xF019120D, // window bg (warm)
|
||||
0xF0201711, // header
|
||||
0x60C3652C, // stroke
|
||||
0xFF221711, // surface
|
||||
0xFF2B1E16, // hover
|
||||
0x50E08C3A, // card enabled overlay
|
||||
0x40302620, // card disabled
|
||||
0xFFFFF7EB, // text primary
|
||||
0x88E3C9A7, // text muted
|
||||
0xFFE69A43, // accent
|
||||
0x80E69A43, // accent soft
|
||||
0x60F5D7B0 // stroke soft
|
||||
);
|
||||
static final Theme NOIR = new Theme(
|
||||
0xF00C0D10, // window bg (deep charcoal)
|
||||
0xF0121317, // header
|
||||
0x60393F4A, // stroke
|
||||
0xFF14161B, // surface
|
||||
0xFF1C1F26, // hover
|
||||
0x5039424F, // card enabled overlay
|
||||
0x40282D35, // card disabled
|
||||
0xFFF4F4F4, // text primary
|
||||
0x88C7CBD3, // text muted
|
||||
0xFFE16B78, // accent
|
||||
0x80E16B78, // accent soft
|
||||
0x605A6370 // stroke soft
|
||||
);
|
||||
static final Theme PURPLE = new Theme(
|
||||
0xF0120E1A, // window bg (purple)
|
||||
0xF0181323, // header
|
||||
0x60433A5A, // stroke
|
||||
0xFF1C1627, // surface
|
||||
0xFF261D35, // hover
|
||||
0x505E4AA0, // card enabled overlay
|
||||
0x40312644, // card disabled
|
||||
0xFFF5F2FF, // text primary
|
||||
0x88CFC6E6, // text muted
|
||||
0xFF9B6BFF, // accent
|
||||
0x809B6BFF, // accent soft
|
||||
0x60705A9C // stroke soft
|
||||
);
|
||||
static final Theme BLUE = new Theme(
|
||||
0xF0080F1E, // window bg (deep blue)
|
||||
0xF00D1626, // header
|
||||
0x60405A82, // stroke
|
||||
0xFF101B2B, // surface
|
||||
0xFF162338, // hover
|
||||
0x50406BB3, // card enabled overlay
|
||||
0x4023324A, // card disabled
|
||||
0xFFF0F5FF, // text primary
|
||||
0x88B2C4E6, // text muted
|
||||
0xFF2E64CC, // accent
|
||||
0x803A6FD6, // accent soft
|
||||
0x60465F8E // stroke soft
|
||||
);
|
||||
static final ThemeEntry CLASSIC_ENTRY = new ThemeEntry(
|
||||
Themes.THEME_CLASSIC,
|
||||
"Classic",
|
||||
true,
|
||||
CLASSIC,
|
||||
Themes.flatGradient(CLASSIC.windowBg()),
|
||||
Themes.subtleHeaderGradient(CLASSIC.windowHeader()),
|
||||
Themes.flatGradient(CLASSIC.surface()),
|
||||
Themes.flatGradient(CLASSIC.cardEnabled()),
|
||||
Themes.flatGradient(CLASSIC.windowStroke())
|
||||
);
|
||||
static final ThemeEntry LEGACY_ENTRY = new ThemeEntry(
|
||||
Themes.THEME_LEGACY,
|
||||
"Legacy",
|
||||
true,
|
||||
LEGACY,
|
||||
Themes.flatGradient(LEGACY.windowBg()),
|
||||
Themes.subtleHeaderGradient(LEGACY.windowHeader()),
|
||||
Themes.flatGradient(LEGACY.surface()),
|
||||
Themes.flatGradient(LEGACY.cardEnabled()),
|
||||
Themes.flatGradient(LEGACY.windowStroke())
|
||||
);
|
||||
static final ThemeEntry AMBER_ENTRY = new ThemeEntry(
|
||||
Themes.THEME_AMBER,
|
||||
"Amber",
|
||||
true,
|
||||
AMBER,
|
||||
Themes.flatGradient(AMBER.windowBg()),
|
||||
Themes.subtleHeaderGradient(AMBER.windowHeader()),
|
||||
Themes.flatGradient(AMBER.surface()),
|
||||
Themes.flatGradient(AMBER.cardEnabled()),
|
||||
Themes.flatGradient(AMBER.windowStroke())
|
||||
);
|
||||
static final ThemeEntry NOIR_ENTRY = new ThemeEntry(
|
||||
Themes.THEME_NOIR,
|
||||
"Noir",
|
||||
true,
|
||||
NOIR,
|
||||
Themes.flatGradient(NOIR.windowBg()),
|
||||
Themes.subtleHeaderGradient(NOIR.windowHeader()),
|
||||
Themes.flatGradient(NOIR.surface()),
|
||||
Themes.flatGradient(NOIR.cardEnabled()),
|
||||
Themes.flatGradient(NOIR.windowStroke())
|
||||
);
|
||||
static final ThemeEntry PURPLE_ENTRY = new ThemeEntry(
|
||||
Themes.THEME_PURPLE,
|
||||
"Purple",
|
||||
true,
|
||||
PURPLE,
|
||||
Themes.flatGradient(PURPLE.windowBg()),
|
||||
Themes.subtleHeaderGradient(PURPLE.windowHeader()),
|
||||
Themes.flatGradient(PURPLE.surface()),
|
||||
Themes.flatGradient(PURPLE.cardEnabled()),
|
||||
Themes.flatGradient(PURPLE.windowStroke())
|
||||
);
|
||||
static final ThemeEntry BLUE_ENTRY = new ThemeEntry(
|
||||
Themes.THEME_BLUE,
|
||||
"Blue",
|
||||
true,
|
||||
BLUE,
|
||||
Themes.flatGradient(BLUE.windowBg()),
|
||||
Themes.subtleHeaderGradient(BLUE.windowHeader()),
|
||||
Themes.flatGradient(BLUE.surface()),
|
||||
Themes.flatGradient(BLUE.cardEnabled()),
|
||||
Themes.flatGradient(BLUE.windowStroke())
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
package dev.loki.lovisual.features.theme;
|
||||
|
||||
import dev.loki.lovisual.features.theme.Themes.Theme;
|
||||
import dev.loki.lovisual.features.theme.Themes.ThemeEntry;
|
||||
import dev.loki.lovisual.features.theme.Themes.GradientSpec;
|
||||
|
||||
/**
|
||||
* Пресет тем 2: AZURE, NITRO, SUNSET, AURORA, MINT, PEACH.
|
||||
*/
|
||||
final class ThemePresets2 {
|
||||
private ThemePresets2() {}
|
||||
|
||||
static final Theme AZURE = new Theme(
|
||||
0xF00C1220, // window bg (blue)
|
||||
0xF0101A2A, // header
|
||||
0x60324B66, // stroke
|
||||
0xFF121C2C, // surface
|
||||
0xFF18253A, // hover
|
||||
0x503F6BAA, // card enabled overlay
|
||||
0x40283348, // card disabled
|
||||
0xFFF2F7FF, // text primary
|
||||
0x88BBD0EA, // text muted
|
||||
0xFF5DA7FF, // accent
|
||||
0x805DA7FF, // accent soft
|
||||
0x60507CA8 // stroke soft
|
||||
);
|
||||
static final Theme NITRO = new Theme(
|
||||
0xFF121624, // window bg
|
||||
0xFF181E33, // header
|
||||
0x60465580, // stroke
|
||||
0xFF1A1F36, // surface
|
||||
0xFF232A45, // hover
|
||||
0x50A24CFF, // card enabled overlay
|
||||
0x40303A52, // card disabled
|
||||
0xFFF3F6FF, // text primary
|
||||
0x88C7D3F3, // text muted
|
||||
0xFF7A5CFF, // accent
|
||||
0x807A5CFF, // accent soft
|
||||
0x60829AC9 // stroke soft
|
||||
);
|
||||
static final Theme SUNSET = new Theme(
|
||||
0xFF1A1116, // window bg
|
||||
0xFF211319, // header
|
||||
0x605C2B2B, // stroke
|
||||
0xFF24151C, // surface
|
||||
0xFF2E1A23, // hover
|
||||
0x50FF7A45, // card enabled overlay
|
||||
0x40261A1A, // card disabled
|
||||
0xFFFFF2E8, // text primary
|
||||
0x88E3B9A3, // text muted
|
||||
0xFFFF7A45, // accent
|
||||
0x80FF7A45, // accent soft
|
||||
0x60F5C1A6 // stroke soft
|
||||
);
|
||||
static final Theme AURORA = new Theme(
|
||||
0xFF0D1A18, // window bg
|
||||
0xFF102320, // header
|
||||
0x602B5B57, // stroke
|
||||
0xFF132623, // surface
|
||||
0xFF1A312D, // hover
|
||||
0x5034D399, // card enabled overlay
|
||||
0x40304742, // card disabled
|
||||
0xFFF1FFF9, // text primary
|
||||
0x8897E3D0, // text muted
|
||||
0xFF2DD9C3, // accent
|
||||
0x802DD9C3, // accent soft
|
||||
0x6071B5A6 // stroke soft
|
||||
);
|
||||
static final Theme MINT = new Theme(
|
||||
0xFF0E1917, // window bg
|
||||
0xFF112320, // header
|
||||
0x60255B53, // stroke
|
||||
0xFF132521, // surface
|
||||
0xFF1A2F2B, // hover
|
||||
0x5035CFA8, // card enabled overlay
|
||||
0x40304742, // card disabled
|
||||
0xFFF1FFF9, // text primary
|
||||
0x8897E3D0, // text muted
|
||||
0xFF43E0B2, // accent
|
||||
0x8043E0B2, // accent soft
|
||||
0x6071B5A6 // stroke soft
|
||||
);
|
||||
static final Theme PEACH = new Theme(
|
||||
0xFF1A1210, // window bg
|
||||
0xFF221510, // header
|
||||
0x605C3A2A, // stroke
|
||||
0xFF241814, // surface
|
||||
0xFF2F211A, // hover
|
||||
0x50F2A269, // card enabled overlay
|
||||
0x40382A22, // card disabled
|
||||
0xFFFFF7F0, // text primary
|
||||
0x88E3C9A7, // text muted
|
||||
0xFFFFA46B, // accent
|
||||
0x80FFB680, // accent soft
|
||||
0x60F5D7B0 // stroke soft
|
||||
);
|
||||
static final ThemeEntry AZURE_ENTRY = new ThemeEntry(
|
||||
Themes.THEME_AZURE,
|
||||
"Azure",
|
||||
true,
|
||||
AZURE,
|
||||
Themes.flatGradient(AZURE.windowBg()),
|
||||
Themes.subtleHeaderGradient(AZURE.windowHeader()),
|
||||
Themes.flatGradient(AZURE.surface()),
|
||||
Themes.flatGradient(AZURE.cardEnabled()),
|
||||
Themes.flatGradient(AZURE.windowStroke())
|
||||
);
|
||||
static final ThemeEntry NITRO_ENTRY = new ThemeEntry(
|
||||
Themes.THEME_NITRO,
|
||||
"Nitro",
|
||||
true,
|
||||
NITRO,
|
||||
new GradientSpec(true, 0xFF36205F, 0xFF0B7DFF, 45f),
|
||||
new GradientSpec(true, 0xFF4B2FAF, 0xFF24C1FF, 45f),
|
||||
new GradientSpec(true, 0xFF1A1F36, 0xFF121728, 90f),
|
||||
new GradientSpec(true, 0xFF5A37D5, 0xFF2AD1FF, 45f),
|
||||
new GradientSpec(true, 0xFF6C4BFF, 0xFF4BD0FF, 45f)
|
||||
);
|
||||
static final ThemeEntry SUNSET_ENTRY = new ThemeEntry(
|
||||
Themes.THEME_SUNSET,
|
||||
"Sunset",
|
||||
true,
|
||||
SUNSET,
|
||||
new GradientSpec(true, 0xFF2A1020, 0xFFB2431F, 45f),
|
||||
new GradientSpec(true, 0xFF3B1626, 0xFFC25B2A, 45f),
|
||||
new GradientSpec(true, 0xFF24151C, 0xFF1E1116, 90f),
|
||||
new GradientSpec(true, 0xFFFF8A4C, 0xFFFF4B7A, 45f),
|
||||
new GradientSpec(true, 0xFFFFB27A, 0xFFFF6A3A, 45f)
|
||||
);
|
||||
static final ThemeEntry AURORA_ENTRY = new ThemeEntry(
|
||||
Themes.THEME_AURORA,
|
||||
"Aurora",
|
||||
true,
|
||||
AURORA,
|
||||
new GradientSpec(true, 0xFF0B2A3C, 0xFF2A8E6B, 120f),
|
||||
new GradientSpec(true, 0xFF0E3B46, 0xFF1FB88B, 120f),
|
||||
new GradientSpec(true, 0xFF132623, 0xFF10201D, 90f),
|
||||
new GradientSpec(true, 0xFF2DD9C3, 0xFF5A7CFF, 135f),
|
||||
new GradientSpec(true, 0xFF78FFE5, 0xFF6FB1FF, 120f)
|
||||
);
|
||||
static final ThemeEntry MINT_ENTRY = new ThemeEntry(
|
||||
Themes.THEME_MINT,
|
||||
"Mint",
|
||||
true,
|
||||
MINT,
|
||||
new GradientSpec(true, 0xFF0B2A24, 0xFF1B3E36, 120f),
|
||||
new GradientSpec(true, 0xFF0F3A32, 0xFF1A6E59, 120f),
|
||||
new GradientSpec(true, 0xFF132521, 0xFF0F1E1B, 90f),
|
||||
new GradientSpec(true, 0xFF3DE3B4, 0xFFB7F2D9, 45f),
|
||||
new GradientSpec(true, 0xFF48E6B9, 0xFF7EF0D6, 120f)
|
||||
);
|
||||
static final ThemeEntry PEACH_ENTRY = new ThemeEntry(
|
||||
Themes.THEME_PEACH,
|
||||
"Peach",
|
||||
true,
|
||||
PEACH,
|
||||
new GradientSpec(true, 0xFF2A1512, 0xFF4B241A, 45f),
|
||||
new GradientSpec(true, 0xFF3B1B14, 0xFF6A301F, 45f),
|
||||
new GradientSpec(true, 0xFF241814, 0xFF1C1310, 90f),
|
||||
new GradientSpec(true, 0xFFFFB47A, 0xFFFF7FA8, 45f),
|
||||
new GradientSpec(true, 0xFFFFC28E, 0xFFFF8A5A, 45f)
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
package dev.loki.lovisual.features.theme;
|
||||
|
||||
import dev.loki.lovisual.features.theme.Themes.Theme;
|
||||
import dev.loki.lovisual.features.theme.Themes.ThemeEntry;
|
||||
import dev.loki.lovisual.features.theme.Themes.GradientSpec;
|
||||
|
||||
/**
|
||||
* Пресет тем 3: LILAC, SAND, DUSK, PRISM, CINDER, FOREST.
|
||||
*/
|
||||
final class ThemePresets3 {
|
||||
private ThemePresets3() {}
|
||||
|
||||
static final Theme LILAC = new Theme(
|
||||
0xFF14131E, // window bg
|
||||
0xFF1A1728, // header
|
||||
0x60433A5A, // stroke
|
||||
0xFF1B172A, // surface
|
||||
0xFF241D36, // hover
|
||||
0x506C5CBE, // card enabled overlay
|
||||
0x40312644, // card disabled
|
||||
0xFFF5F2FF, // text primary
|
||||
0x88CFC6E6, // text muted
|
||||
0xFFB79BFF, // accent
|
||||
0x80C8B0FF, // accent soft
|
||||
0x60705A9C // stroke soft
|
||||
);
|
||||
static final Theme SAND = new Theme(
|
||||
0xFF181510, // window bg
|
||||
0xFF201A13, // header
|
||||
0x6050432F, // stroke
|
||||
0xFF221B14, // surface
|
||||
0xFF2C231B, // hover
|
||||
0x50E8C98C, // card enabled overlay
|
||||
0x40322A21, // card disabled
|
||||
0xFFFFF4E8, // text primary
|
||||
0x88D8C4A2, // text muted
|
||||
0xFFE8C98C, // accent
|
||||
0x80F0D8A8, // accent soft
|
||||
0x607A6A54 // stroke soft
|
||||
);
|
||||
static final Theme DUSK = new Theme(
|
||||
0xFF141018, // window bg
|
||||
0xFF1B1422, // header
|
||||
0x60503A4A, // stroke
|
||||
0xFF1D1626, // surface
|
||||
0xFF271B34, // hover
|
||||
0x50FF875A, // card enabled overlay
|
||||
0x4030263B, // card disabled
|
||||
0xFFF6F1FF, // text primary
|
||||
0x88D6C9E6, // text muted
|
||||
0xFFFF8A4C, // accent
|
||||
0x80FF9B5A, // accent soft
|
||||
0x606B5A7A // stroke soft
|
||||
);
|
||||
static final Theme PRISM = new Theme(
|
||||
0xFF0F1220, // window bg
|
||||
0xFF141A2B, // header
|
||||
0x60435173, // stroke
|
||||
0xFF151C2E, // surface
|
||||
0xFF1B243A, // hover
|
||||
0x504B7CFF, // card enabled overlay
|
||||
0x4031324A, // card disabled
|
||||
0xFFF3F6FF, // text primary
|
||||
0x88C2CDEB, // text muted
|
||||
0xFF5AC8FA, // accent
|
||||
0x8074B6FF, // accent soft
|
||||
0x606B7A9E // stroke soft
|
||||
);
|
||||
static final Theme CINDER = new Theme(
|
||||
0xFF120E0F, // window bg
|
||||
0xFF181012, // header
|
||||
0x604A2F32, // stroke
|
||||
0xFF1A1214, // surface
|
||||
0xFF23171A, // hover
|
||||
0x50D24A4A, // card enabled overlay
|
||||
0x40281A1C, // card disabled
|
||||
0xFFFFF3F3, // text primary
|
||||
0x88E3B9B9, // text muted
|
||||
0xFFDC4A4A, // accent
|
||||
0x80E06A6A, // accent soft
|
||||
0x606B4E52 // stroke soft
|
||||
);
|
||||
static final Theme FOREST = new Theme(
|
||||
0xFF101611, // window bg
|
||||
0xFF141E15, // header
|
||||
0x6043573D, // stroke
|
||||
0xFF161F18, // surface
|
||||
0xFF1D2B20, // hover
|
||||
0x5071C36C, // card enabled overlay
|
||||
0x40263225, // card disabled
|
||||
0xFFF3FFF1, // text primary
|
||||
0x88C5E3C1, // text muted
|
||||
0xFF7DD36B, // accent
|
||||
0x807DD36B, // accent soft
|
||||
0x60607B58 // stroke soft
|
||||
);
|
||||
static final ThemeEntry LILAC_ENTRY = new ThemeEntry(
|
||||
Themes.THEME_LILAC,
|
||||
"Lilac",
|
||||
true,
|
||||
LILAC,
|
||||
new GradientSpec(true, 0xFF241A3B, 0xFF1A3A55, 120f),
|
||||
new GradientSpec(true, 0xFF2F214A, 0xFF3D2F6A, 120f),
|
||||
new GradientSpec(true, 0xFF1B172A, 0xFF15121F, 90f),
|
||||
new GradientSpec(true, 0xFFB79BFF, 0xFF7AD9FF, 45f),
|
||||
new GradientSpec(true, 0xFFC5B0FF, 0xFF8BB2FF, 120f)
|
||||
);
|
||||
static final ThemeEntry SAND_ENTRY = new ThemeEntry(
|
||||
Themes.THEME_SAND,
|
||||
"Sand",
|
||||
true,
|
||||
SAND,
|
||||
new GradientSpec(true, 0xFF2A2016, 0xFF3A2F22, 45f),
|
||||
new GradientSpec(true, 0xFF32261B, 0xFF4A3A2B, 45f),
|
||||
new GradientSpec(true, 0xFF221B14, 0xFF1A1510, 90f),
|
||||
new GradientSpec(true, 0xFFF2D29B, 0xFFE8B67A, 45f),
|
||||
new GradientSpec(true, 0xFFF5E0B2, 0xFFD1A36C, 45f)
|
||||
);
|
||||
static final ThemeEntry DUSK_ENTRY = new ThemeEntry(
|
||||
Themes.THEME_DUSK,
|
||||
"Dusk",
|
||||
true,
|
||||
DUSK,
|
||||
new GradientSpec(true, 0xFF2B1630, 0xFF6B2A1F, 45f),
|
||||
new GradientSpec(true, 0xFF3B1B3A, 0xFF8A3B24, 45f),
|
||||
new GradientSpec(true, 0xFF1D1626, 0xFF141018, 90f),
|
||||
new GradientSpec(true, 0xFF7C4BFF, 0xFFFF9B3D, 45f),
|
||||
new GradientSpec(true, 0xFFA775FF, 0xFFFF7A3A, 45f)
|
||||
);
|
||||
static final ThemeEntry PRISM_ENTRY = new ThemeEntry(
|
||||
Themes.THEME_PRISM,
|
||||
"Prism",
|
||||
true,
|
||||
PRISM,
|
||||
new GradientSpec(true, 0xFF1A243A, 0xFF2A1E4A, 135f),
|
||||
new GradientSpec(true, 0xFF223050, 0xFF352060, 135f),
|
||||
new GradientSpec(true, 0xFF151C2E, 0xFF101522, 90f),
|
||||
new GradientSpec(true, 0xFF59C4FF, 0xFFB35CFF, 45f),
|
||||
new GradientSpec(true, 0xFF7AD4FF, 0xFFE36BFF, 45f)
|
||||
);
|
||||
static final ThemeEntry CINDER_ENTRY = new ThemeEntry(
|
||||
Themes.THEME_CINDER,
|
||||
"Cinder",
|
||||
true,
|
||||
CINDER,
|
||||
new GradientSpec(true, 0xFF1A0C0C, 0xFF2A1416, 45f),
|
||||
new GradientSpec(true, 0xFF241012, 0xFF3A191C, 45f),
|
||||
new GradientSpec(true, 0xFF1A1214, 0xFF120E0F, 90f),
|
||||
new GradientSpec(true, 0xFFDC4A4A, 0xFF401010, 45f),
|
||||
new GradientSpec(true, 0xFFE26A6A, 0xFF7A2A2A, 45f)
|
||||
);
|
||||
static final ThemeEntry FOREST_ENTRY = new ThemeEntry(
|
||||
Themes.THEME_FOREST,
|
||||
"Forest",
|
||||
true,
|
||||
FOREST,
|
||||
new GradientSpec(true, 0xFF1A241C, 0xFF2B3A24, 120f),
|
||||
new GradientSpec(true, 0xFF1F2B21, 0xFF2F4B2E, 120f),
|
||||
new GradientSpec(true, 0xFF161F18, 0xFF101611, 90f),
|
||||
new GradientSpec(true, 0xFF7DD36B, 0xFFC6E886, 45f),
|
||||
new GradientSpec(true, 0xFF9BE07C, 0xFF6BC26A, 45f)
|
||||
);
|
||||
}
|
||||
|
|
@ -7,16 +7,12 @@
|
|||
|
||||
package dev.loki.lovisual.mixins.entity.inventory;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.ModifyReturnValue;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import dev.loki.lovisual.util.player.inventory.core.InventorySwap;
|
||||
|
||||
@Mixin(Inventory.class)
|
||||
public abstract class PlayerInventoryMixin {
|
||||
|
|
@ -28,19 +24,6 @@ public abstract class PlayerInventoryMixin {
|
|||
@Shadow
|
||||
public abstract ItemStack getItem(int slot);
|
||||
|
||||
@ModifyReturnValue(method = "getSelectedSlot", at = @At("RETURN"))
|
||||
private int lovisual$silentSelectedSlot(int original) {
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
return player == mc.player ? InventorySwap.INSTANCE.effectiveSelectedSlot() : original;
|
||||
}
|
||||
// Hotbar lease removed (was InventorySwap) — keep original behavior
|
||||
|
||||
@ModifyReturnValue(method = "getSelectedItem", at = @At("RETURN"))
|
||||
private ItemStack lovisual$silentSelectedStack(ItemStack original) {
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
if (player == mc.player && InventorySwap.INSTANCE.isHotbarLeased()) {
|
||||
return getItem(InventorySwap.INSTANCE.effectiveSelectedSlot());
|
||||
}
|
||||
|
||||
return original;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
package dev.loki.lovisual.mixins.entity.living;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
|
||||
import com.llamalad7.mixinextras.injector.ModifyReturnValue;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.player.LocalPlayer;
|
||||
import net.minecraft.core.BlockPos;
|
||||
|
|
@ -48,7 +47,6 @@ import dev.loki.lovisual.features.module.modules.visuals.weather.WorldTweaks;
|
|||
import dev.loki.lovisual.util.aiming.rotation.RotationManager;
|
||||
import dev.loki.lovisual.util.aiming.data.Rotation;
|
||||
import dev.loki.lovisual.util.aiming.features.MovementCorrection;
|
||||
import dev.loki.lovisual.util.player.inventory.core.InventorySwap;
|
||||
import dev.loki.lovisual.util.pvp.client.CooldownsState;
|
||||
import dev.loki.lovisual.util.pvp.opponents.TotemPopCounter;
|
||||
|
||||
|
|
@ -76,16 +74,6 @@ public abstract class LivingEntityMixin {
|
|||
|
||||
}
|
||||
|
||||
@ModifyReturnValue(method = "getMainHandItem", at = @At("RETURN"))
|
||||
private ItemStack lovisual$applyInventorySwapLeaseForMainHand(ItemStack original) {
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
if ((Object) this == mc.player) {
|
||||
InventorySwap.INSTANCE.isHotbarLeased();
|
||||
}
|
||||
|
||||
return original;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional local cooldown synthesis for servers that lock consumables without sending vanilla cooldown packets.
|
||||
* This never cancels item use; it only starts a visible local timer when explicitly configured.
|
||||
|
|
|
|||
|
|
@ -8,11 +8,14 @@
|
|||
package dev.loki.lovisual.mixins.gui.core;
|
||||
|
||||
import dev.loki.lovisual.features.module.lifecycle.Modules;
|
||||
import dev.loki.lovisual.features.module.modules.misc.esp.ShitEsp;
|
||||
import dev.loki.lovisual.features.module.modules.misc.items.ItemScroller;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.player.Animations;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.items.ShulkerPreview;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||
import net.minecraft.world.inventory.Slot;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.joml.Matrix3x2fStack;
|
||||
|
|
@ -30,6 +33,15 @@ public abstract class AbstractContainerScreenMixin {
|
|||
@Shadow
|
||||
protected Slot hoveredSlot;
|
||||
|
||||
@Shadow
|
||||
protected AbstractContainerMenu menu;
|
||||
|
||||
@Shadow
|
||||
protected int leftPos;
|
||||
|
||||
@Shadow
|
||||
protected int topPos;
|
||||
|
||||
@Unique
|
||||
private boolean lovisual$animations$scaleApplied;
|
||||
|
||||
|
|
@ -118,4 +130,22 @@ public abstract class AbstractContainerScreenMixin {
|
|||
cir.setReturnValue(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Inject(method = "extractRenderState(Lnet/minecraft/client/gui/GuiGraphicsExtractor;IIF)V", at = @At("TAIL"))
|
||||
private void lovisual$shitEsp$highlight(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float partialTick, CallbackInfo ci) {
|
||||
ShitEsp esp = Modules.get(ShitEsp.class);
|
||||
if (esp == null || !esp.isEnabled() || !esp.hasAnyGroup() || menu == null) return;
|
||||
for (Slot slot : menu.slots) {
|
||||
if (slot == null || !slot.hasItem()) continue;
|
||||
ItemStack stack = slot.getItem();
|
||||
Integer col = esp.colorFor(stack);
|
||||
if (col == null) continue;
|
||||
int x1 = leftPos + slot.x;
|
||||
int y1 = topPos + slot.y;
|
||||
int x2 = x1 + 16;
|
||||
int y2 = y1 + 16;
|
||||
graphics.fill(RenderPipelines.GUI, x1, y1, x2, y2, col);
|
||||
graphics.outline(x1, y1, x2 - x1, y2 - y1, col | 0xFF000000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import org.spongepowered.asm.mixin.injection.At;
|
|||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
import dev.loki.lovisual.features.module.lifecycle.Modules;
|
||||
import dev.loki.lovisual.features.module.modules.misc.BetterMinecraft;
|
||||
import dev.loki.lovisual.features.module.modules.misc.vanilla.BetterMinecraft;
|
||||
|
||||
@Mixin(AbstractRecipeBookScreen.class)
|
||||
public abstract class RecipeBookScreenMixin {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import org.spongepowered.asm.mixin.injection.At;
|
|||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
import dev.loki.lovisual.features.module.lifecycle.Modules;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.scene.Chams;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.scene.visual.Chams;
|
||||
import dev.loki.lovisual.render.iris.runtime.IrisRuntime;
|
||||
|
||||
@Mixin(value = GameRenderer.class, priority = 900)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
package dev.loki.lovisual.mixins.network.game;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.multiplayer.MultiPlayerGameMode;
|
||||
import net.minecraft.client.player.LocalPlayer;
|
||||
|
|
@ -34,10 +33,9 @@ import dev.loki.lovisual.features.module.modules.combat.damage.NoFriendDamage;
|
|||
import dev.loki.lovisual.features.module.modules.combat.pvp.PvpCooldowns;
|
||||
import dev.loki.lovisual.features.module.modules.misc.sound.HitSounds;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.camera.Freecam;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.effects.HitBubbles;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.effects.HitEffect;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.effects.hits.HitBubbles;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.effects.hits.HitEffect;
|
||||
import dev.loki.lovisual.mixins.accessors.world.WorldAccessor;
|
||||
import dev.loki.lovisual.util.player.inventory.core.InventorySwap;
|
||||
import dev.loki.lovisual.util.combat.tracking.ReachMeter;
|
||||
import dev.loki.lovisual.util.target.TargetManager;
|
||||
|
||||
|
|
@ -58,17 +56,6 @@ public class MultiPlayerGameModeMixin {
|
|||
}
|
||||
}
|
||||
|
||||
@ModifyExpressionValue(
|
||||
method = "ensureHasSentCarriedItem",
|
||||
at = @At(
|
||||
value = "INVOKE",
|
||||
target = "Lnet/minecraft/world/entity/player/Inventory;getSelectedSlot()I"
|
||||
)
|
||||
)
|
||||
private int lovisual$hookSilentSelectedSlot(int original) {
|
||||
return InventorySwap.INSTANCE.effectiveSelectedSlot();
|
||||
}
|
||||
|
||||
@Inject(method = "destroyBlock", at = @At("RETURN"))
|
||||
private void lovisual$breakBlockEvent(BlockPos pos, CallbackInfoReturnable<Boolean> cir) {
|
||||
if (!cir.getReturnValue()) return;
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import org.spongepowered.asm.mixin.injection.Inject;
|
|||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
import dev.loki.lovisual.features.gui.hud.nondraggable.impl.tab.CustomTabList;
|
||||
import dev.loki.lovisual.features.module.lifecycle.Modules;
|
||||
import dev.loki.lovisual.features.module.modules.misc.BetterMinecraft;
|
||||
import dev.loki.lovisual.features.module.modules.misc.vanilla.BetterMinecraft;
|
||||
import dev.loki.lovisual.features.module.modules.misc.protect.NameProtect;
|
||||
import dev.loki.lovisual.features.relations.category.CategoryService;
|
||||
import dev.loki.lovisual.features.relations.category.CategoryType;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.mixins.render.entity;
|
||||
package dev.loki.lovisual.mixins.render.entity.crystal;
|
||||
|
||||
import dev.loki.lovisual.features.module.lifecycle.Modules;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.scene.EndCrystal;
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.mixins.render.entity;
|
||||
package dev.loki.lovisual.mixins.render.entity.crystal;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.mixins.render.entity;
|
||||
package dev.loki.lovisual.mixins.render.entity.item;
|
||||
|
||||
import net.minecraft.client.renderer.entity.state.ItemEntityRenderState;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.mixins.render.entity;
|
||||
package dev.loki.lovisual.mixins.render.entity.item;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.math.Axis;
|
||||
|
|
@ -18,7 +18,7 @@ import org.spongepowered.asm.mixin.injection.At;
|
|||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
import dev.loki.lovisual.features.module.lifecycle.Modules;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.weather.AtmoDawnFog;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.weather.sky.AtmoDawnFog;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.weather.WorldTweaks;
|
||||
|
||||
@Mixin(value = AtmosphericFogEnvironment.class, priority = 1200)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import org.spongepowered.asm.mixin.injection.Inject;
|
|||
import org.spongepowered.asm.mixin.injection.ModifyVariable;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
import dev.loki.lovisual.features.module.lifecycle.Modules;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.weather.AtmoDawnFog;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.weather.sky.AtmoDawnFog;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.weather.WorldTweaks;
|
||||
|
||||
@Mixin(value = FogRenderer.class, priority = 1200)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ package dev.loki.lovisual.mixins.render.item;
|
|||
|
||||
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
|
||||
import dev.loki.lovisual.features.module.lifecycle.Modules;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.items.AlwaysVisibleHand;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.items.held.AlwaysVisibleHand;
|
||||
import net.minecraft.client.renderer.ItemInHandRenderer;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
|
|
|
|||
|
|
@ -26,12 +26,12 @@ import org.spongepowered.asm.mixin.injection.Inject;
|
|||
import org.spongepowered.asm.mixin.injection.ModifyVariable;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
import dev.loki.lovisual.features.module.lifecycle.Modules;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.items.HoldMyItems;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.items.held.HoldMyItems;
|
||||
import dev.loki.lovisual.util.animation.hand.HandDynamics;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.camera.ViewModel;
|
||||
import dev.loki.lovisual.util.animation.EquipProgressComposer;
|
||||
import dev.loki.lovisual.util.animation.hand.state.HandAnimState;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.scene.Chams;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.scene.visual.Chams;
|
||||
|
||||
@Environment(EnvType.CLIENT)
|
||||
@Mixin(ItemInHandRenderer.class)
|
||||
|
|
|
|||
|
|
@ -36,8 +36,8 @@ import org.spongepowered.asm.mixin.injection.Inject;
|
|||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
import dev.loki.lovisual.features.module.lifecycle.Modules;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.scene.BlockHighlight;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.scene.ReimaginedVisual;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.weather.ShaderSky;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.scene.visual.ReimaginedVisual;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.weather.sky.ShaderSky;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.weather.WorldTweaks;
|
||||
import dev.loki.lovisual.mixins.accessors.renderer.GameRendererAccessor;
|
||||
import dev.loki.lovisual.mixins.accessors.world.LevelRendererAccessor;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.mixins.render.level;
|
||||
package dev.loki.lovisual.mixins.render.level.particles;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
* Licensed under the GNU General Public License v3.0.
|
||||
*/
|
||||
|
||||
package dev.loki.lovisual.mixins.render.level;
|
||||
package dev.loki.lovisual.mixins.render.level.particles;
|
||||
|
||||
import net.minecraft.client.particle.Particle;
|
||||
import net.minecraft.client.particle.ParticleEngine;
|
||||
|
|
@ -15,7 +15,7 @@ import org.spongepowered.asm.mixin.injection.At;
|
|||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
import dev.loki.lovisual.events.Events;
|
||||
import dev.loki.lovisual.events.impl.render.ParticleSpawnEvent;
|
||||
import dev.loki.lovisual.events.impl.render.particles.ParticleSpawnEvent;
|
||||
import dev.loki.lovisual.util.player.effect.heuristics.StatusEffectHeuristics;
|
||||
|
||||
@Mixin(ParticleEngine.class)
|
||||
|
|
@ -77,8 +77,8 @@ 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.Chams;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.scene.ReimaginedVisual;
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -22,12 +22,12 @@ import dev.loki.lovisual.render.engine.rhi.pipeline.layout.RenderPipelineRegistr
|
|||
import dev.loki.lovisual.render.engine.shader.LoVisualShaderSources;
|
||||
import dev.loki.lovisual.render.engine.vertex.LoVisualVertexFormats;
|
||||
import dev.loki.lovisual.util.logging.DebugLog;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.effects.KillEffect;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.effects.hits.KillEffect;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.esp.ESP;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.fx.MotionBlur;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.fx.PostFX;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.scene.BlockHighlight;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.weather.ShaderSky;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.weather.sky.ShaderSky;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.packs.resources.ResourceManager;
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import com.mojang.blaze3d.vertex.DefaultVertexFormat;
|
|||
import dev.loki.lovisual.render.engine.pipeline.ExtendedRenderPipelineBuilder;
|
||||
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
|
||||
import dev.loki.lovisual.render.engine.vertex.LoVisualVertexFormats;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.effects.KillEffect;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.effects.hits.KillEffect;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.scene.BlockHighlight;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import com.mojang.blaze3d.textures.GpuTextureView;
|
|||
import net.minecraft.client.Minecraft;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import dev.loki.lovisual.features.module.lifecycle.Modules;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.scene.ReimaginedVisual;
|
||||
import dev.loki.lovisual.features.module.modules.visuals.scene.visual.ReimaginedVisual;
|
||||
import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem;
|
||||
import dev.loki.lovisual.render.engine.depth.PreTranslucentDepth;
|
||||
import dev.loki.lovisual.render.engine.depth.WorldSceneDepth;
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ import dev.loki.lovisual.render.engine.RenderState;
|
|||
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
|
||||
import dev.loki.lovisual.render.engine.renderer.FullScreenRenderer;
|
||||
import dev.loki.lovisual.render.engine.renderer.MeshRenderer;
|
||||
import dev.loki.lovisual.render.engine.uniform.impl.sky.SkyShaderNebulaUniforms;
|
||||
import dev.loki.lovisual.render.engine.uniform.impl.sky.SkyShaderUniforms;
|
||||
import dev.loki.lovisual.render.engine.uniform.impl.sky.postprocess.SkyShaderNebulaUniforms;
|
||||
import dev.loki.lovisual.render.engine.uniform.impl.sky.postprocess.SkyShaderUniforms;
|
||||
import net.minecraft.client.Camera;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.ProjectionMatrixBuffer;
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
|
|||
import dev.loki.lovisual.render.sky.CustomSkyboxRenderer;
|
||||
import dev.loki.lovisual.render.engine.renderer.FullScreenRenderer;
|
||||
import dev.loki.lovisual.render.engine.renderer.MeshRenderer;
|
||||
import dev.loki.lovisual.render.engine.uniform.impl.sky.SkyShaderStarryUniforms;
|
||||
import dev.loki.lovisual.render.engine.uniform.impl.sky.postprocess.SkyShaderStarryUniforms;
|
||||
import net.minecraft.client.Camera;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.ProjectionMatrixBuffer;
|
||||
|
|
|
|||
|
|
@ -7,73 +7,37 @@
|
|||
|
||||
package dev.loki.lovisual.render.engine.renderer.ui.batches.ordered;
|
||||
|
||||
import com.mojang.blaze3d.pipeline.RenderPipeline;
|
||||
import com.mojang.blaze3d.pipeline.RenderTarget;
|
||||
import com.mojang.blaze3d.pipeline.TextureTarget;
|
||||
import com.mojang.blaze3d.textures.GpuSampler;
|
||||
import com.mojang.blaze3d.textures.GpuTextureView;
|
||||
import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem;
|
||||
import com.mojang.blaze3d.pipeline.RenderPipeline;
|
||||
import dev.loki.lovisual.render.engine.core.context.ViewportContext;
|
||||
import dev.loki.lovisual.render.helpers.clip.ScissorFunction;
|
||||
import dev.loki.lovisual.render.helpers.clip.ClipFunction;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.gui.navigation.ScreenRectangle;
|
||||
import net.minecraft.client.renderer.Projection;
|
||||
import net.minecraft.client.renderer.texture.AbstractTexture;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.joml.Matrix4f;
|
||||
import dev.loki.lovisual.render.engine.core.frame.RenderPhase;
|
||||
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
|
||||
import dev.loki.lovisual.render.engine.postprocess.post.PostProcessManager;
|
||||
import dev.loki.lovisual.render.engine.renderer.MeshRenderer;
|
||||
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
|
||||
import dev.loki.lovisual.render.engine.text.glyph.GlyphFont;
|
||||
import dev.loki.lovisual.render.engine.text.render.TextRenderSystem;
|
||||
import dev.loki.lovisual.render.engine.text.backend.placement.TextPlacementMode;
|
||||
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
|
||||
import dev.loki.lovisual.render.engine.uniform.impl.ui.MsdfTextUniforms;
|
||||
import dev.loki.lovisual.render.engine.uniform.impl.ui.UIBatchUniforms;
|
||||
import dev.loki.lovisual.render.engine.uniform.impl.ui.UIBlurUniforms;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.batches.DrawBatch;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.batches.ItemBatch;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.batches.TextBatch;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.blur.FrameBlurCacheEntry;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.blur.UiBlurResources;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.deferred.DeferredOrderedSubmit;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.batches.ordered.flush.OrderedDeferral;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.batches.ordered.flush.OrderedFlushRenderer;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.batches.ordered.order.OrderState;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.blur.kawase.SharedKawaseBlur;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.deferred.sched.UiDeferredScheduler;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.dispatch.UiRenderDispatcher;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.item.ItemBatchRenderer;
|
||||
import dev.loki.lovisual.render.engine.text.backend.placement.TextPlacementMode;
|
||||
import dev.loki.lovisual.render.engine.text.glyph.GlyphFont;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Координатор упорядоченного UI-батчинга: аккумулирует записи в OrderState,
|
||||
* при flush диспетчеризует их через OrderedFlushRenderer либо уходит в
|
||||
* deferred-слой через OrderedDeferral. Разделяемый блюр — SharedKawaseBlur.
|
||||
*/
|
||||
public final class OrderedUiBatcher {
|
||||
private static final OrderedUiBatcher LIQUID_GLASS_PREWARMER = new OrderedUiBatcher();
|
||||
final ObjectArrayList<Object> order = new ObjectArrayList<>(128);
|
||||
@SuppressWarnings("unchecked")
|
||||
final ObjectArrayList<DrawBatch>[] pools = new ObjectArrayList[UiBatchType.values().length];
|
||||
final ObjectArrayList<ItemBatch> itemPool = new ObjectArrayList<>(32);
|
||||
final ObjectArrayList<TextBatch> textPool = new ObjectArrayList<>(32);
|
||||
// Text must remain in the exact draw order. Chat, nametags, layered HUD widgets and
|
||||
// marquee/clip stacks rely on text being interleaved with shapes/items. Only adjacent
|
||||
// compatible text runs are merged; text is never moved across another draw entry.
|
||||
final int[] poolCursor = new int[UiBatchType.values().length];
|
||||
int itemPoolCursor;
|
||||
int textPoolCursor;
|
||||
private final OrderState state = new OrderState();
|
||||
private final SharedKawaseBlur blur = new SharedKawaseBlur();
|
||||
boolean active;
|
||||
boolean flushing;
|
||||
public @Nullable GpuTextureView sharedBlurSourceView;
|
||||
public @Nullable GpuSampler sharedBlurSourceSampler;
|
||||
public @Nullable GpuTextureView sharedBlurredView;
|
||||
public @Nullable GpuSampler sharedBlurredSampler;
|
||||
Renderer2D.BlurQuality sharedBlurQuality = Renderer2D.DEFAULT_BLUR_QUALITY;
|
||||
float sharedBlurOffsetPx = Renderer2D.DEFAULT_KAWASE_OFFSET_PX;
|
||||
|
||||
public OrderedUiBatcher() {
|
||||
for (int i = 0; i < pools.length; i++) {
|
||||
pools[i] = new ObjectArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
public static void prewarmLiquidGlassBlur(Minecraft mc,
|
||||
@Nullable GpuTextureView sourceView,
|
||||
|
|
@ -81,24 +45,12 @@ public final class OrderedUiBatcher {
|
|||
float screenW,
|
||||
float screenH,
|
||||
float uiScale) {
|
||||
if (mc == null || sourceView == null || sourceSampler == null) return;
|
||||
LIQUID_GLASS_PREWARMER.resetSharedBlur();
|
||||
LIQUID_GLASS_PREWARMER.prepareSharedBlurInternal(
|
||||
mc,
|
||||
sourceView,
|
||||
sourceSampler,
|
||||
screenW,
|
||||
screenH,
|
||||
uiScale,
|
||||
Renderer2D.DEFAULT_LIQUID_GLASS_BLUR_QUALITY,
|
||||
Renderer2D.LIQUID_GLASS_KAWASE_OFFSET_PX
|
||||
);
|
||||
LIQUID_GLASS_PREWARMER.resetSharedBlur();
|
||||
SharedKawaseBlur.prewarmLiquidGlassBlur(mc, sourceView, sourceSampler, screenW, screenH, uiScale);
|
||||
}
|
||||
|
||||
public void begin() {
|
||||
resetOrder();
|
||||
resetSharedBlur();
|
||||
state.resetOrder();
|
||||
blur.reset();
|
||||
active = true;
|
||||
Renderer2D.BATCH_STATS.setActive(true);
|
||||
}
|
||||
|
|
@ -112,246 +64,63 @@ public final class OrderedUiBatcher {
|
|||
}
|
||||
|
||||
public boolean hasPendingWork() {
|
||||
return !order.isEmpty();
|
||||
return state.hasPendingWork();
|
||||
}
|
||||
|
||||
public DrawBatch getOrCreate(UiBatchType type, GpuTextureView view, GpuSampler sampler) {
|
||||
if (!active) return null;
|
||||
boolean shapeClipActive = ClipFunction.isShapeClipActive();
|
||||
if (!order.isEmpty()) {
|
||||
Object last = order.get(order.size() - 1);
|
||||
if (last instanceof DrawBatch drawBatch && drawBatch.canMerge(type, view, sampler, shapeClipActive)) return drawBatch;
|
||||
}
|
||||
DrawBatch batch = obtain(type);
|
||||
batch.begin(view, sampler, shapeClipActive);
|
||||
order.add(batch);
|
||||
UiRenderDispatcher.recordBackendCommand(type);
|
||||
return batch;
|
||||
return state.getOrCreate(type, view, sampler);
|
||||
}
|
||||
|
||||
public DrawBatch getOrCreateBlur(UiBatchType type, GpuTextureView view, GpuSampler sampler,
|
||||
Renderer2D.BlurQuality quality, float offsetPx) {
|
||||
if (!active) return null;
|
||||
if (type == UiBatchType.LIQUID_GLASS) {
|
||||
UiBlurResources.requestLiquidGlassBlur();
|
||||
}
|
||||
boolean shapeClipActive = ClipFunction.isShapeClipActive();
|
||||
Renderer2D.BlurQuality normalizedQuality = quality != null ? quality : Renderer2D.DEFAULT_BLUR_QUALITY;
|
||||
float normalizedOffset = Float.isFinite(offsetPx) ? Math.max(0.0f, offsetPx) : Renderer2D.DEFAULT_KAWASE_OFFSET_PX;
|
||||
if (!order.isEmpty()) {
|
||||
Object last = order.get(order.size() - 1);
|
||||
if (last instanceof DrawBatch drawBatch
|
||||
&& drawBatch.canMergeBlur(type, view, sampler, normalizedQuality, normalizedOffset, shapeClipActive)) {
|
||||
return drawBatch;
|
||||
}
|
||||
}
|
||||
DrawBatch batch = obtain(type);
|
||||
batch.beginBlur(view, sampler, normalizedQuality, normalizedOffset, shapeClipActive);
|
||||
order.add(batch);
|
||||
UiRenderDispatcher.recordBackendCommand(type);
|
||||
return batch;
|
||||
return state.getOrCreateBlur(type, view, sampler, quality, offsetPx);
|
||||
}
|
||||
|
||||
public DrawBatch getOrCreateMsdf(GpuTextureView view, GpuSampler sampler, float pxRange, int atlasWidth, int atlasHeight) {
|
||||
if (!active) return null;
|
||||
boolean shapeClipActive = ClipFunction.isShapeClipActive();
|
||||
if (!order.isEmpty()) {
|
||||
Object last = order.get(order.size() - 1);
|
||||
if (last instanceof DrawBatch drawBatch && drawBatch.canMergeMsdf(view, sampler, pxRange, atlasWidth, atlasHeight, shapeClipActive)) {
|
||||
return drawBatch;
|
||||
}
|
||||
}
|
||||
DrawBatch batch = obtain(UiBatchType.SVG_MSDF);
|
||||
batch.beginMsdf(view, sampler, pxRange, atlasWidth, atlasHeight, shapeClipActive);
|
||||
order.add(batch);
|
||||
UiRenderDispatcher.recordBackendCommand(UiBatchType.SVG_MSDF);
|
||||
return batch;
|
||||
return state.getOrCreateMsdf(view, sampler, pxRange, atlasWidth, atlasHeight);
|
||||
}
|
||||
|
||||
public ItemBatch getOrCreateItemBatch(@Nullable GuiGraphicsExtractor context,
|
||||
ItemStack stack) {
|
||||
public ItemBatch getOrCreateItemBatch(@Nullable GuiGraphicsExtractor context, ItemStack stack) {
|
||||
if (!active) return null;
|
||||
ScreenRectangle scissor = context != null ? context.scissorStack.peek() : null;
|
||||
if (!order.isEmpty()) {
|
||||
Object last = order.get(order.size() - 1);
|
||||
if (last instanceof ItemBatch itemBatch && itemBatch.canMerge(context, scissor)) {
|
||||
return itemBatch;
|
||||
}
|
||||
}
|
||||
ItemBatch batch = obtainItemBatch();
|
||||
batch.begin(context, scissor);
|
||||
order.add(batch);
|
||||
UiRenderDispatcher.recordBackendCommand("ITEM");
|
||||
return batch;
|
||||
return state.getOrCreateItemBatch(context, stack);
|
||||
}
|
||||
|
||||
public TextBatch getOrCreateTextBatch(String label, GlyphFont font, RenderPipeline pipeline,
|
||||
TextPlacementMode placement, boolean liquidGlassText) {
|
||||
if (!active) return null;
|
||||
if (liquidGlassText) {
|
||||
UiBlurResources.requestLiquidGlassBlur();
|
||||
}
|
||||
TextPlacementMode normalizedPlacement = placement != null ? placement : TextPlacementMode.UI;
|
||||
boolean shapeClipActive = ClipFunction.isShapeClipActive();
|
||||
|
||||
if (!order.isEmpty()) {
|
||||
Object last = order.get(order.size() - 1);
|
||||
if (last instanceof TextBatch textBatch
|
||||
&& textBatch.canMerge(font, pipeline, normalizedPlacement, liquidGlassText, shapeClipActive)) {
|
||||
return textBatch;
|
||||
}
|
||||
}
|
||||
|
||||
TextBatch batch = obtainTextBatch();
|
||||
batch.begin(label, font, pipeline, normalizedPlacement, liquidGlassText, shapeClipActive);
|
||||
order.add(batch);
|
||||
UiRenderDispatcher.recordBackendCommand(liquidGlassText ? "LIQUID_GLASS_TEXT" : "TEXT");
|
||||
return batch;
|
||||
}
|
||||
|
||||
DrawBatch obtain(UiBatchType type) {
|
||||
int idx = type.ordinal();
|
||||
ObjectArrayList<DrawBatch> pool = pools[idx];
|
||||
int cursor = poolCursor[idx];
|
||||
DrawBatch batch;
|
||||
if (cursor < pool.size()) {
|
||||
batch = pool.get(cursor);
|
||||
} else {
|
||||
batch = new DrawBatch(type);
|
||||
pool.add(batch);
|
||||
}
|
||||
poolCursor[idx] = cursor + 1;
|
||||
return batch;
|
||||
}
|
||||
|
||||
ItemBatch obtainItemBatch() {
|
||||
int cursor = itemPoolCursor;
|
||||
ItemBatch batch;
|
||||
if (cursor < itemPool.size()) {
|
||||
batch = itemPool.get(cursor);
|
||||
} else {
|
||||
batch = new ItemBatch();
|
||||
itemPool.add(batch);
|
||||
}
|
||||
itemPoolCursor = cursor + 1;
|
||||
return batch;
|
||||
}
|
||||
|
||||
TextBatch obtainTextBatch() {
|
||||
int cursor = textPoolCursor;
|
||||
TextBatch batch;
|
||||
if (cursor < textPool.size()) {
|
||||
batch = textPool.get(cursor);
|
||||
} else {
|
||||
batch = new TextBatch();
|
||||
textPool.add(batch);
|
||||
}
|
||||
textPoolCursor = cursor + 1;
|
||||
return batch;
|
||||
}
|
||||
|
||||
int flushLiquidGlassTextBatch(TextBatch textBatch,
|
||||
Minecraft mc,
|
||||
GpuTextureView mainColorView,
|
||||
@Nullable GpuTextureView liquidSourceView,
|
||||
@Nullable GpuSampler liquidSourceSampler,
|
||||
float screenW,
|
||||
float screenH,
|
||||
float uiScale) {
|
||||
if (textBatch == null || textBatch.mesh == null || textBatch.font == null) return 0;
|
||||
if (!textBatch.font.isReady()) return 0;
|
||||
|
||||
AbstractTexture glyphTexture = textBatch.font.getTexture();
|
||||
if (glyphTexture == null || glyphTexture.getTextureView() == null || glyphTexture.getSampler() == null) return 0;
|
||||
|
||||
GpuTextureView sourceView = liquidSourceView;
|
||||
GpuSampler sourceSampler = liquidSourceSampler;
|
||||
if (sourceView == null || sourceSampler == null || sourceView == mainColorView) return 0;
|
||||
|
||||
int blurPassCalls = 0;
|
||||
if (textBatch.shapeClipActive) {
|
||||
adoptFrameBlurCache(sourceView, sourceSampler, screenW, screenH, uiScale,
|
||||
Renderer2D.DEFAULT_LIQUID_GLASS_BLUR_QUALITY, Renderer2D.LIQUID_GLASS_KAWASE_OFFSET_PX);
|
||||
} else {
|
||||
blurPassCalls = prepareSharedBlur(mc, sourceView, sourceSampler, screenW, screenH, uiScale,
|
||||
Renderer2D.DEFAULT_LIQUID_GLASS_BLUR_QUALITY, Renderer2D.LIQUID_GLASS_KAWASE_OFFSET_PX);
|
||||
}
|
||||
GpuTextureView blurView = matchingSharedBlurView(sourceView, sourceSampler);
|
||||
GpuSampler blurSampler = matchingSharedBlurSampler(sourceView, sourceSampler);
|
||||
if (blurView == null || blurSampler == null) {
|
||||
blurView = sourceView;
|
||||
blurSampler = sourceSampler;
|
||||
}
|
||||
if (blurView == mainColorView) return 0;
|
||||
|
||||
UIBatchUniforms.update(screenW, screenH);
|
||||
MeshRenderer builder = MeshRenderer.begin()
|
||||
.attachments(mainColorView, null)
|
||||
.pipeline(textBatch.pipeline)
|
||||
.mesh(textBatch.mesh)
|
||||
.uniform("UIBatch", UIBatchUniforms.get())
|
||||
.sampler("u_Texture", glyphTexture.getTextureView(), glyphTexture.getSampler())
|
||||
.sampler("u_SceneTexture", sourceView, sourceSampler)
|
||||
.sampler("u_BlurTexture", blurView, blurSampler);
|
||||
|
||||
if (textBatch.font.isMsdf()) {
|
||||
MsdfTextUniforms.update(textBatch.font.getPxRange(), textBatch.font.getAtlasWidth(), textBatch.font.getAtlasHeight());
|
||||
builder.uniform("MsdfText", MsdfTextUniforms.get());
|
||||
}
|
||||
|
||||
builder.end();
|
||||
return 1 + blurPassCalls;
|
||||
return state.getOrCreateTextBatch(label, font, pipeline, placement, liquidGlassText);
|
||||
}
|
||||
|
||||
public void flush(boolean finish) {
|
||||
if (!active) return;
|
||||
if (this == Renderer2D.UI_BATCHER && UiDeferredScheduler.shouldDefer()) {
|
||||
deferCurrent(finish);
|
||||
OrderedDeferral.defer(this, state, blur, finish);
|
||||
return;
|
||||
}
|
||||
flushing = true;
|
||||
try {
|
||||
if (order.isEmpty()) {
|
||||
Renderer2D.BATCH_STATS.noteEmpty(active, poolTotal());
|
||||
if (finish) {
|
||||
resetSharedBlur();
|
||||
active = false;
|
||||
Renderer2D.BATCH_STATS.setActive(false);
|
||||
}
|
||||
if (!state.hasPendingWork()) {
|
||||
Renderer2D.BATCH_STATS.noteEmpty(active, state.poolTotal());
|
||||
finishPhase(finish);
|
||||
return;
|
||||
}
|
||||
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
if (mc == null) {
|
||||
Renderer2D.BATCH_STATS.noteFailure("mc null");
|
||||
resetOrder();
|
||||
resetSharedBlur();
|
||||
if (finish) {
|
||||
active = false;
|
||||
Renderer2D.BATCH_STATS.setActive(false);
|
||||
}
|
||||
flushFailed("mc null", finish);
|
||||
return;
|
||||
}
|
||||
RenderTarget fb = mc.gameRenderer.mainRenderTarget();
|
||||
if (fb == null) {
|
||||
Renderer2D.BATCH_STATS.noteFailure("framebuffer null");
|
||||
resetOrder();
|
||||
resetSharedBlur();
|
||||
if (finish) {
|
||||
active = false;
|
||||
Renderer2D.BATCH_STATS.setActive(false);
|
||||
}
|
||||
flushFailed("framebuffer null", finish);
|
||||
return;
|
||||
}
|
||||
GpuTextureView mainColorView = fb.getColorTextureView();
|
||||
if (mainColorView == null) {
|
||||
Renderer2D.BATCH_STATS.noteFailure("framebuffer color view null");
|
||||
resetOrder();
|
||||
resetSharedBlur();
|
||||
if (finish) {
|
||||
active = false;
|
||||
Renderer2D.BATCH_STATS.setActive(false);
|
||||
}
|
||||
flushFailed("framebuffer color view null", finish);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -359,586 +128,60 @@ public final class OrderedUiBatcher {
|
|||
float screenH = mc.getWindow().getHeight();
|
||||
float uiScale = ViewportContext.getUiScale();
|
||||
if (uiScale <= 0.0f) uiScale = 1.0f;
|
||||
com.mojang.blaze3d.buffers.GpuBufferSlice uiBatch = null;
|
||||
resetSharedBlur();
|
||||
blur.reset();
|
||||
|
||||
boolean hasLiquidGlass = false;
|
||||
for (Object orderedEntry : order) {
|
||||
if (orderedEntry instanceof DrawBatch orderedBatch && orderedBatch.type == UiBatchType.LIQUID_GLASS) {
|
||||
hasLiquidGlass = true;
|
||||
break;
|
||||
}
|
||||
if (orderedEntry instanceof TextBatch orderedText && orderedText.liquidGlassText) {
|
||||
hasLiquidGlass = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
OrderedFlushRenderer.FlushResult result =
|
||||
OrderedFlushRenderer.flush(state, blur, mc, mainColorView, screenW, screenH, uiScale);
|
||||
|
||||
GpuTextureView liquidSourceView = null;
|
||||
GpuSampler liquidSourceSampler = null;
|
||||
int batches = state.entryCount();
|
||||
Renderer2D.BATCH_STATS.update(active, batches, result.drawCalls(), result.vertices(), result.indices(), state.poolTotal());
|
||||
Renderer2D.BATCH_STATS.addFrame(batches, result.drawCalls(), result.vertices(), result.indices());
|
||||
|
||||
int drawCalls = 0;
|
||||
int vertices = 0;
|
||||
int indices = 0;
|
||||
|
||||
TextureTarget fxBuffer = null;
|
||||
GpuTextureView fxView = null;
|
||||
GpuSampler fxSampler = null;
|
||||
MeshBuilder fxCompositeMesh = null;
|
||||
|
||||
if (hasLiquidGlass) {
|
||||
/*
|
||||
* The HUD/world glass source is captured before GUI extraction. Never perform a
|
||||
* fallback copy of the active main framebuffer from inside a batch flush: doing so
|
||||
* inserts a render-target transition in the middle of the HUD phase and can stall the
|
||||
* frame or temporarily discard already submitted lower layers.
|
||||
*/
|
||||
TextureTarget sourceBuffer = UiBlurResources.ensureGlassSource(mc);
|
||||
GpuSampler copySampler = PostProcessManager.getSampler();
|
||||
if (UiBlurResources.isWorldSourceReady() && sourceBuffer != null && copySampler != null) {
|
||||
GpuTextureView sourceTargetView = sourceBuffer.getColorTextureView();
|
||||
if (sourceTargetView != null && sourceTargetView != mainColorView) {
|
||||
liquidSourceView = sourceTargetView;
|
||||
liquidSourceSampler = copySampler;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (Object entry : order) {
|
||||
if (entry instanceof ItemBatch itemBatch) {
|
||||
if (itemBatch.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
drawCalls += ItemBatchRenderer.flush(itemBatch);
|
||||
continue;
|
||||
}
|
||||
if (entry instanceof TextBatch textBatch) {
|
||||
if (!textBatch.isEmpty()) {
|
||||
vertices += textBatch.mesh.getVertexCount();
|
||||
indices += textBatch.mesh.getIndicesCount();
|
||||
if (textBatch.liquidGlassText) {
|
||||
drawCalls += flushLiquidGlassTextBatch(textBatch, mc, mainColorView,
|
||||
liquidSourceView, liquidSourceSampler, screenW, screenH, uiScale);
|
||||
} else {
|
||||
drawCalls++;
|
||||
TextRenderSystem.submitGlyphMeshImmediate(textBatch.label, textBatch.font, textBatch.mesh, textBatch.pipeline, textBatch.placement);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
DrawBatch batch = (DrawBatch) entry;
|
||||
if (batch.mesh.isBuilding()) batch.mesh.end();
|
||||
if (batch.mesh.getIndicesCount() <= 0) continue;
|
||||
|
||||
drawCalls++;
|
||||
vertices += batch.mesh.getVertexCount();
|
||||
indices += batch.mesh.getIndicesCount();
|
||||
|
||||
if (batch.type == UiBatchType.BLUR || batch.type == UiBatchType.GLASS_BLUR || batch.type == UiBatchType.BLUR_COMPLEX) {
|
||||
int blurPassCalls = prepareSharedBlur(mc, batch.view, batch.sampler, screenW, screenH, uiScale,
|
||||
batch.blurQuality, batch.blurOffsetPx);
|
||||
if (sharedBlurredView != null && sharedBlurredSampler != null) {
|
||||
MeshRenderer builder = MeshRenderer.begin()
|
||||
.attachments(mainColorView, null)
|
||||
.pipeline(batch.type.pipeline)
|
||||
.mesh(batch.mesh);
|
||||
|
||||
if (uiBatch == null) {
|
||||
UIBatchUniforms.update(screenW, screenH);
|
||||
uiBatch = UIBatchUniforms.get();
|
||||
}
|
||||
builder.uniform("UIBatch", uiBatch);
|
||||
builder.sampler("u_Texture", sharedBlurredView, sharedBlurredSampler);
|
||||
builder.end();
|
||||
|
||||
drawCalls += blurPassCalls;
|
||||
continue;
|
||||
}
|
||||
if (batch.view == mainColorView) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (batch.type == UiBatchType.LIQUID_GLASS) {
|
||||
GpuTextureView sourceView = liquidSourceView != null ? liquidSourceView : batch.view;
|
||||
GpuSampler sourceSampler = liquidSourceSampler != null ? liquidSourceSampler : batch.sampler;
|
||||
boolean clippedComposite = batch.shapeClipActive;
|
||||
if (clippedComposite) {
|
||||
adoptFrameBlurCache(sourceView, sourceSampler, screenW, screenH, uiScale,
|
||||
batch.blurQuality, batch.blurOffsetPx);
|
||||
} else {
|
||||
drawCalls += prepareSharedBlur(mc, sourceView, sourceSampler, screenW, screenH, uiScale,
|
||||
batch.blurQuality, batch.blurOffsetPx);
|
||||
}
|
||||
GpuTextureView liquidBlurView = matchingSharedBlurView(sourceView, sourceSampler);
|
||||
GpuSampler liquidBlurSampler = matchingSharedBlurSampler(sourceView, sourceSampler);
|
||||
if (liquidBlurView == null || liquidBlurSampler == null) {
|
||||
liquidBlurView = sourceView;
|
||||
liquidBlurSampler = sourceSampler;
|
||||
}
|
||||
|
||||
if (sourceView == null || sourceSampler == null
|
||||
|| liquidBlurView == null || liquidBlurSampler == null
|
||||
|| sourceView == mainColorView
|
||||
|| liquidBlurView == mainColorView) {
|
||||
continue;
|
||||
}
|
||||
|
||||
MeshRenderer directBuilder = MeshRenderer.begin()
|
||||
.attachments(mainColorView, null)
|
||||
.pipeline(batch.type.pipeline)
|
||||
.mesh(batch.mesh);
|
||||
|
||||
if (uiBatch == null) {
|
||||
UIBatchUniforms.update(screenW, screenH);
|
||||
uiBatch = UIBatchUniforms.get();
|
||||
}
|
||||
directBuilder.uniform("UIBatch", uiBatch);
|
||||
directBuilder.sampler("u_Texture", sourceView, sourceSampler);
|
||||
directBuilder.sampler("u_BlurTexture", liquidBlurView, liquidBlurSampler);
|
||||
directBuilder.end();
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean isFx = batch.type == UiBatchType.BLUR || batch.type == UiBatchType.GLASS_BLUR || batch.type == UiBatchType.BLUR_COMPLEX;
|
||||
if (isFx) {
|
||||
if (fxBuffer == null) {
|
||||
fxBuffer = UiBlurResources.ensureEffects(mc);
|
||||
if (fxBuffer != null) {
|
||||
fxView = fxBuffer.getColorTextureView();
|
||||
fxSampler = PostProcessManager.getSampler();
|
||||
int compW = Math.max(1, Math.round(screenW / uiScale));
|
||||
int compH = Math.max(1, Math.round(screenH / uiScale));
|
||||
fxCompositeMesh = UiBlurResources.ensureCompositeMesh(compW, compH);
|
||||
}
|
||||
}
|
||||
if (fxBuffer != null && fxView != null && fxSampler != null && fxCompositeMesh != null) {
|
||||
MeshRenderer fxBuilder = MeshRenderer.begin()
|
||||
.attachments(fxBuffer)
|
||||
.clearColor(0x00000000)
|
||||
.pipeline(batch.type.pipeline)
|
||||
.mesh(batch.mesh);
|
||||
|
||||
if (batch.type.needsUiBatch) {
|
||||
if (uiBatch == null) {
|
||||
UIBatchUniforms.update(screenW, screenH);
|
||||
uiBatch = UIBatchUniforms.get();
|
||||
}
|
||||
fxBuilder.uniform("UIBatch", uiBatch);
|
||||
}
|
||||
if (batch.type.usesSampler) {
|
||||
GpuTextureView sourceView = batch.type == UiBatchType.LIQUID_GLASS && liquidSourceView != null ? liquidSourceView : batch.view;
|
||||
GpuSampler sourceSampler = batch.type == UiBatchType.LIQUID_GLASS && liquidSourceSampler != null ? liquidSourceSampler : batch.sampler;
|
||||
fxBuilder.sampler("u_Texture", sourceView, sourceSampler);
|
||||
}
|
||||
if (batch.type == UiBatchType.LIQUID_GLASS) {
|
||||
GpuTextureView liquidBlurView = sharedBlurredView != null ? sharedBlurredView : batch.view;
|
||||
GpuSampler liquidBlurSampler = sharedBlurredSampler != null ? sharedBlurredSampler : batch.sampler;
|
||||
if (liquidBlurView != null && liquidBlurSampler != null) {
|
||||
fxBuilder.sampler("u_BlurTexture", liquidBlurView, liquidBlurSampler);
|
||||
}
|
||||
}
|
||||
|
||||
fxBuilder.end();
|
||||
|
||||
MeshRenderer.begin()
|
||||
.attachments(mainColorView, null)
|
||||
.pipeline(LoVisualRenderPipelines.UI_TEXTURED)
|
||||
.mesh(fxCompositeMesh)
|
||||
.sampler("u_Texture", fxView, fxSampler)
|
||||
.end();
|
||||
|
||||
drawCalls++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Never sample from the main color attachment while writing to it.
|
||||
// If offscreen FX target is unavailable, skip this pass to avoid undefined behavior.
|
||||
if (batch.type.usesSampler
|
||||
&& batch.view == mainColorView
|
||||
&& !(batch.type == UiBatchType.LIQUID_GLASS && liquidSourceView != null)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if ((batch.type == UiBatchType.BLUR || batch.type == UiBatchType.GLASS_BLUR || batch.type == UiBatchType.BLUR_COMPLEX || batch.type == UiBatchType.LIQUID_GLASS)
|
||||
&& batch.type.usesSampler
|
||||
&& batch.view == mainColorView
|
||||
&& !(batch.type == UiBatchType.LIQUID_GLASS && liquidSourceView != null)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
MeshRenderer builder = MeshRenderer.begin()
|
||||
.attachments(mainColorView, null)
|
||||
.pipeline(batch.type.pipeline)
|
||||
.mesh(batch.mesh);
|
||||
|
||||
if (batch.type.needsUiBatch) {
|
||||
if (uiBatch == null) {
|
||||
UIBatchUniforms.update(screenW, screenH);
|
||||
uiBatch = UIBatchUniforms.get();
|
||||
}
|
||||
builder.uniform("UIBatch", uiBatch);
|
||||
}
|
||||
if (batch.type.usesSampler) {
|
||||
GpuTextureView sourceView = batch.type == UiBatchType.LIQUID_GLASS && liquidSourceView != null ? liquidSourceView : batch.view;
|
||||
GpuSampler sourceSampler = batch.type == UiBatchType.LIQUID_GLASS && liquidSourceSampler != null ? liquidSourceSampler : batch.sampler;
|
||||
builder.sampler("u_Texture", sourceView, sourceSampler);
|
||||
}
|
||||
if (batch.type == UiBatchType.SVG_MSDF) {
|
||||
MsdfTextUniforms.update(batch.msdfPxRange, batch.msdfAtlasWidth, batch.msdfAtlasHeight);
|
||||
builder.uniform("MsdfText", MsdfTextUniforms.get());
|
||||
}
|
||||
if (batch.type == UiBatchType.LIQUID_GLASS) {
|
||||
GpuTextureView liquidBlurView = sharedBlurredView != null ? sharedBlurredView : batch.view;
|
||||
GpuSampler liquidBlurSampler = sharedBlurredSampler != null ? sharedBlurredSampler : batch.sampler;
|
||||
if (liquidBlurView != null && liquidBlurSampler != null) {
|
||||
builder.sampler("u_BlurTexture", liquidBlurView, liquidBlurSampler);
|
||||
}
|
||||
}
|
||||
|
||||
builder.end();
|
||||
}
|
||||
|
||||
int batches = order.size();
|
||||
Renderer2D.BATCH_STATS.update(active, batches, drawCalls, vertices, indices, poolTotal());
|
||||
Renderer2D.BATCH_STATS.addFrame(batches, drawCalls, vertices, indices);
|
||||
|
||||
resetOrder();
|
||||
if (finish) {
|
||||
resetSharedBlur();
|
||||
active = false;
|
||||
Renderer2D.BATCH_STATS.setActive(false);
|
||||
}
|
||||
state.resetOrder();
|
||||
finishPhase(finish);
|
||||
} finally {
|
||||
flushing = false;
|
||||
}
|
||||
}
|
||||
|
||||
boolean isPureItemBatchOrder() {
|
||||
if (order.isEmpty()) return false;
|
||||
for (int i = 0, size = order.size(); i < size; i++) {
|
||||
if (!(order.get(i) instanceof ItemBatch)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void deferCurrent(boolean finish) {
|
||||
if (order.isEmpty()) {
|
||||
Renderer2D.BATCH_STATS.noteEmpty(active, poolTotal());
|
||||
if (finish) {
|
||||
resetSharedBlur();
|
||||
active = false;
|
||||
Renderer2D.BATCH_STATS.setActive(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
int batches = order.size();
|
||||
int vertices = 0;
|
||||
int indices = 0;
|
||||
for (int i = 0; i < order.size(); i++) {
|
||||
Object entry = order.get(i);
|
||||
if (entry instanceof DrawBatch drawBatch) {
|
||||
if (drawBatch.mesh.isBuilding()) drawBatch.mesh.end();
|
||||
vertices += drawBatch.mesh.getVertexCount();
|
||||
indices += drawBatch.mesh.getIndicesCount();
|
||||
} else if (entry instanceof TextBatch textBatch && textBatch.mesh != null) {
|
||||
if (textBatch.mesh.isBuilding()) textBatch.mesh.end();
|
||||
vertices += textBatch.mesh.getVertexCount();
|
||||
indices += textBatch.mesh.getIndicesCount();
|
||||
}
|
||||
}
|
||||
|
||||
OrderedUiBatcher submitted = this;
|
||||
UiDeferredScheduler.enqueue(new DeferredOrderedSubmit(
|
||||
UiDeferredScheduler.layerForCurrentPhase(isPureItemBatchOrder()),
|
||||
UiDeferredScheduler.snapshotViewport(),
|
||||
ScissorFunction.currentFramebufferScissor(),
|
||||
submitted
|
||||
));
|
||||
|
||||
OrderedUiBatcher replacement = UiDeferredScheduler.obtainBatcher();
|
||||
replacement.prepareAsReplacement(!finish);
|
||||
Renderer2D.UI_BATCHER = replacement;
|
||||
|
||||
Renderer2D.BATCH_STATS.update(active, batches, 0, vertices, indices, poolTotal());
|
||||
Renderer2D.BATCH_STATS.setActive(!finish);
|
||||
}
|
||||
|
||||
void prepareAsReplacement(boolean active) {
|
||||
resetOrder();
|
||||
resetSharedBlur();
|
||||
public void prepareAsReplacement(boolean active) {
|
||||
state.resetOrder();
|
||||
blur.reset();
|
||||
this.active = active;
|
||||
}
|
||||
|
||||
public void releaseAfterDeferred() {
|
||||
resetOrder();
|
||||
resetSharedBlur();
|
||||
state.resetOrder();
|
||||
blur.reset();
|
||||
active = false;
|
||||
flushing = false;
|
||||
}
|
||||
|
||||
private boolean adoptFrameBlurCache(@Nullable GpuTextureView sourceView,
|
||||
@Nullable GpuSampler sourceSampler,
|
||||
float screenW,
|
||||
float screenH,
|
||||
float uiScale,
|
||||
Renderer2D.BlurQuality quality,
|
||||
float offsetPx) {
|
||||
if (sourceView == null || sourceSampler == null) return false;
|
||||
FrameBlurCacheEntry cachedBlur = UiBlurResources.findReusable(
|
||||
UiBlurResources.currentFrameId(),
|
||||
UiBlurResources.currentPhase(),
|
||||
sourceView,
|
||||
sourceSampler,
|
||||
screenW,
|
||||
screenH,
|
||||
uiScale,
|
||||
quality,
|
||||
offsetPx
|
||||
);
|
||||
if (cachedBlur == null || cachedBlur.blurredView == null || cachedBlur.blurredSampler == null) {
|
||||
return false;
|
||||
}
|
||||
sharedBlurSourceView = sourceView;
|
||||
sharedBlurSourceSampler = sourceSampler;
|
||||
sharedBlurredView = cachedBlur.blurredView;
|
||||
sharedBlurredSampler = cachedBlur.blurredSampler;
|
||||
sharedBlurQuality = quality != null ? quality : Renderer2D.DEFAULT_BLUR_QUALITY;
|
||||
sharedBlurOffsetPx = Float.isFinite(offsetPx) ? Math.max(0.0f, offsetPx) : Renderer2D.DEFAULT_KAWASE_OFFSET_PX;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
GpuTextureView matchingSharedBlurView(@Nullable GpuTextureView sourceView, @Nullable GpuSampler sourceSampler) {
|
||||
if (sourceView == null || sourceSampler == null) return null;
|
||||
if (sharedBlurredView == null || sharedBlurredSampler == null) return null;
|
||||
if (sharedBlurSourceView != sourceView || sharedBlurSourceSampler != sourceSampler) return null;
|
||||
return sharedBlurredView;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
GpuSampler matchingSharedBlurSampler(@Nullable GpuTextureView sourceView, @Nullable GpuSampler sourceSampler) {
|
||||
if (matchingSharedBlurView(sourceView, sourceSampler) == null) return null;
|
||||
return sharedBlurredSampler;
|
||||
}
|
||||
|
||||
int prepareSharedBlur(Minecraft mc,
|
||||
@Nullable GpuTextureView sourceView,
|
||||
@Nullable GpuSampler sourceSampler,
|
||||
float screenW,
|
||||
float screenH,
|
||||
float uiScale) {
|
||||
return prepareSharedBlur(mc, sourceView, sourceSampler, screenW, screenH, uiScale,
|
||||
Renderer2D.DEFAULT_BLUR_QUALITY, Renderer2D.DEFAULT_KAWASE_OFFSET_PX);
|
||||
}
|
||||
|
||||
int prepareSharedBlur(Minecraft mc,
|
||||
@Nullable GpuTextureView sourceView,
|
||||
@Nullable GpuSampler sourceSampler,
|
||||
float screenW,
|
||||
float screenH,
|
||||
float uiScale,
|
||||
Renderer2D.BlurQuality blurQuality,
|
||||
float offsetPx) {
|
||||
if (LoVisualRenderSystem.rhi().shapeClip().isActive()) {
|
||||
adoptFrameBlurCache(sourceView, sourceSampler, screenW, screenH, uiScale, blurQuality, offsetPx);
|
||||
return 0;
|
||||
}
|
||||
return prepareSharedBlurInternal(mc, sourceView, sourceSampler, screenW, screenH, uiScale, blurQuality, offsetPx);
|
||||
}
|
||||
|
||||
private int prepareSharedBlurInternal(Minecraft mc,
|
||||
@Nullable GpuTextureView sourceView,
|
||||
@Nullable GpuSampler sourceSampler,
|
||||
float screenW,
|
||||
float screenH,
|
||||
float uiScale,
|
||||
Renderer2D.BlurQuality blurQuality,
|
||||
float offsetPx) {
|
||||
if (sourceView == null || sourceSampler == null) {
|
||||
return 0;
|
||||
}
|
||||
Renderer2D.BlurQuality quality = blurQuality != null ? blurQuality : Renderer2D.DEFAULT_BLUR_QUALITY;
|
||||
int iterations = Math.max(1, quality.iterations);
|
||||
float passOffset = Float.isFinite(offsetPx) ? Math.max(0.0f, offsetPx) : Renderer2D.DEFAULT_KAWASE_OFFSET_PX;
|
||||
|
||||
if (sharedBlurredView != null && sharedBlurredSampler != null
|
||||
&& sharedBlurSourceView == sourceView
|
||||
&& sharedBlurSourceSampler == sourceSampler
|
||||
&& sharedBlurQuality == quality
|
||||
&& Float.compare(sharedBlurOffsetPx, passOffset) == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
long frameId = UiBlurResources.currentFrameId();
|
||||
RenderPhase phase = UiBlurResources.currentPhase();
|
||||
FrameBlurCacheEntry cachedBlur = UiBlurResources.findReusable(frameId, phase, sourceView, sourceSampler,
|
||||
screenW, screenH, uiScale, quality, passOffset);
|
||||
if (cachedBlur != null) {
|
||||
sharedBlurSourceView = sourceView;
|
||||
sharedBlurSourceSampler = sourceSampler;
|
||||
sharedBlurredView = cachedBlur.blurredView;
|
||||
sharedBlurredSampler = cachedBlur.blurredSampler;
|
||||
sharedBlurQuality = quality;
|
||||
sharedBlurOffsetPx = passOffset;
|
||||
return 0;
|
||||
}
|
||||
|
||||
resetSharedBlur();
|
||||
|
||||
GpuSampler blurSampler = PostProcessManager.getSampler();
|
||||
if (blurSampler == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
GpuTextureView currentView = sourceView;
|
||||
GpuSampler currentSampler = sourceSampler;
|
||||
int sourceW = Math.max(1, Math.round(screenW));
|
||||
int sourceH = Math.max(1, Math.round(screenH));
|
||||
int drawCalls = 0;
|
||||
|
||||
for (int level = 0; level < iterations; level++) {
|
||||
TextureTarget target = UiBlurResources.ensureKawaseDown(mc, level);
|
||||
if (target == null) return 0;
|
||||
if (!drawKawasePass(target, currentView, currentSampler, sourceW, sourceH, passOffset, false)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
currentView = target.getColorTextureView();
|
||||
currentSampler = blurSampler;
|
||||
sourceW = Math.max(1, target.width);
|
||||
sourceH = Math.max(1, target.height);
|
||||
drawCalls++;
|
||||
if (currentView == null) return 0;
|
||||
}
|
||||
|
||||
for (int level = iterations - 2; level >= 0; level--) {
|
||||
TextureTarget target = UiBlurResources.ensureKawaseUp(mc, level);
|
||||
if (target == null) return 0;
|
||||
if (!drawKawasePass(target, currentView, currentSampler, sourceW, sourceH, passOffset, true)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
currentView = target.getColorTextureView();
|
||||
currentSampler = blurSampler;
|
||||
sourceW = Math.max(1, target.width);
|
||||
sourceH = Math.max(1, target.height);
|
||||
drawCalls++;
|
||||
if (currentView == null) return 0;
|
||||
}
|
||||
|
||||
if (currentView == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
sharedBlurSourceView = sourceView;
|
||||
sharedBlurSourceSampler = sourceSampler;
|
||||
sharedBlurredView = currentView;
|
||||
sharedBlurredSampler = blurSampler;
|
||||
sharedBlurQuality = quality;
|
||||
sharedBlurOffsetPx = passOffset;
|
||||
UiBlurResources.remember(frameId, phase, sourceView, sourceSampler, currentView, blurSampler,
|
||||
screenW, screenH, uiScale, quality, passOffset);
|
||||
return drawCalls;
|
||||
}
|
||||
|
||||
private static boolean drawKawasePass(TextureTarget target,
|
||||
GpuTextureView sourceView,
|
||||
GpuSampler sourceSampler,
|
||||
int sourceW,
|
||||
int sourceH,
|
||||
float passOffset,
|
||||
boolean upPass) {
|
||||
if (target == null || sourceView == null || sourceSampler == null) return false;
|
||||
MeshBuilder compositeMesh = UiBlurResources.ensureCompositeMesh(target.width, target.height);
|
||||
if (compositeMesh == null) return false;
|
||||
|
||||
Matrix4f previousProjection = MeshRenderer.projection();
|
||||
try {
|
||||
MeshRenderer.setProjection(orthoProjection(target.width, target.height));
|
||||
UIBlurUniforms.update(sourceW, sourceH, target.width, target.height,
|
||||
0f, 0f, passOffset, upPass ? 1.0f : 0.0f, 1.0f, 1.0f, 0xFFFFFF);
|
||||
MeshRenderer.begin()
|
||||
.attachments(target)
|
||||
.clearColor(0x00000000)
|
||||
.pipeline(LoVisualRenderPipelines.UI_BLUR)
|
||||
.mesh(compositeMesh)
|
||||
.uniform("UIBlur", UIBlurUniforms.get())
|
||||
.sampler("u_Texture", sourceView, sourceSampler)
|
||||
.end();
|
||||
return target.getColorTextureView() != null;
|
||||
} finally {
|
||||
MeshRenderer.setProjection(previousProjection);
|
||||
}
|
||||
}
|
||||
|
||||
private static Matrix4f orthoProjection(float width, float height) {
|
||||
Projection projection = new Projection();
|
||||
projection.setupOrtho(-1000.0f, 1000.0f, Math.max(1.0f, width), Math.max(1.0f, height), true);
|
||||
return projection.getMatrix(new Matrix4f());
|
||||
}
|
||||
|
||||
void resetSharedBlur() {
|
||||
sharedBlurSourceView = null;
|
||||
sharedBlurSourceSampler = null;
|
||||
sharedBlurredView = null;
|
||||
sharedBlurredSampler = null;
|
||||
sharedBlurQuality = Renderer2D.DEFAULT_BLUR_QUALITY;
|
||||
sharedBlurOffsetPx = Renderer2D.DEFAULT_KAWASE_OFFSET_PX;
|
||||
}
|
||||
|
||||
void resetOrder() {
|
||||
order.clear();
|
||||
for (int i = 0; i < poolCursor.length; i++) {
|
||||
poolCursor[i] = 0;
|
||||
}
|
||||
itemPoolCursor = 0;
|
||||
textPoolCursor = 0;
|
||||
}
|
||||
|
||||
public void closeRetainedBuffers() {
|
||||
resetOrder();
|
||||
resetSharedBlur();
|
||||
state.resetOrder();
|
||||
blur.reset();
|
||||
active = false;
|
||||
flushing = false;
|
||||
|
||||
for (ObjectArrayList<DrawBatch> pool : pools) {
|
||||
for (int i = 0, size = pool.size(); i < size; i++) {
|
||||
DrawBatch batch = pool.get(i);
|
||||
if (batch != null && batch.mesh != null) {
|
||||
batch.mesh.close();
|
||||
}
|
||||
}
|
||||
state.closeRetained();
|
||||
}
|
||||
|
||||
for (int i = 0, size = textPool.size(); i < size; i++) {
|
||||
TextBatch batch = textPool.get(i);
|
||||
if (batch != null && batch.mesh != null) {
|
||||
batch.mesh.close();
|
||||
}
|
||||
public void deactivate() {
|
||||
active = false;
|
||||
Renderer2D.BATCH_STATS.setActive(false);
|
||||
}
|
||||
|
||||
for (int i = 0, size = itemPool.size(); i < size; i++) {
|
||||
ItemBatch batch = itemPool.get(i);
|
||||
if (batch != null) {
|
||||
batch.commands.clear();
|
||||
batch.resolvedStates.clear();
|
||||
batch.context = null;
|
||||
batch.scissor = null;
|
||||
}
|
||||
}
|
||||
private void flushFailed(String reason, boolean finish) {
|
||||
Renderer2D.BATCH_STATS.noteFailure(reason);
|
||||
state.resetOrder();
|
||||
blur.reset();
|
||||
finishPhase(finish);
|
||||
}
|
||||
|
||||
int poolTotal() {
|
||||
int total = 0;
|
||||
for (ObjectArrayList<DrawBatch> pool : pools) {
|
||||
total += pool.size();
|
||||
}
|
||||
return total + itemPool.size() + textPool.size();
|
||||
private void finishPhase(boolean finish) {
|
||||
if (finish) {
|
||||
blur.reset();
|
||||
active = false;
|
||||
Renderer2D.BATCH_STATS.setActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.render.engine.renderer.ui.batches.ordered.flush;
|
||||
|
||||
import com.mojang.blaze3d.textures.GpuSampler;
|
||||
import com.mojang.blaze3d.textures.GpuTextureView;
|
||||
import dev.loki.lovisual.render.engine.renderer.MeshRenderer;
|
||||
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.batches.TextBatch;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.batches.ordered.flush.OrderedFlushRenderer.Ctx;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.blur.kawase.SharedKawaseBlur;
|
||||
import dev.loki.lovisual.render.engine.text.glyph.GlyphFont;
|
||||
import dev.loki.lovisual.render.engine.uniform.impl.ui.MsdfTextUniforms;
|
||||
import dev.loki.lovisual.render.engine.uniform.impl.ui.UIBatchUniforms;
|
||||
import net.minecraft.client.renderer.texture.AbstractTexture;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Отрисовка текстового batch'а с liquid-glass подложкой: текст сэмплит
|
||||
* исходник сцены и разделяемый блюр поверх glyph-текстуры.
|
||||
*/
|
||||
final class LiquidGlassTextFlush {
|
||||
private LiquidGlassTextFlush() {
|
||||
}
|
||||
|
||||
static int flush(TextBatch textBatch, Ctx ctx) {
|
||||
if (textBatch == null || textBatch.mesh == null || textBatch.font == null) return 0;
|
||||
if (!textBatch.font.isReady()) return 0;
|
||||
|
||||
AbstractTexture glyphTexture = textBatch.font.getTexture();
|
||||
if (glyphTexture == null || glyphTexture.getTextureView() == null || glyphTexture.getSampler() == null) return 0;
|
||||
|
||||
GpuTextureView sourceView = ctx.liquidSourceView;
|
||||
GpuSampler sourceSampler = ctx.liquidSourceSampler;
|
||||
if (sourceView == null || sourceSampler == null || sourceView == ctx.mainColorView) return 0;
|
||||
|
||||
SharedKawaseBlur blur = ctx.blur;
|
||||
int blurPassCalls;
|
||||
if (textBatch.shapeClipActive) {
|
||||
blur.adoptCached(sourceView, sourceSampler, ctx.screenW, ctx.screenH, ctx.uiScale,
|
||||
Renderer2D.DEFAULT_LIQUID_GLASS_BLUR_QUALITY, Renderer2D.LIQUID_GLASS_KAWASE_OFFSET_PX);
|
||||
blurPassCalls = 0;
|
||||
} else {
|
||||
blurPassCalls = blur.prepare(ctx.mc, sourceView, sourceSampler, ctx.screenW, ctx.screenH, ctx.uiScale,
|
||||
Renderer2D.DEFAULT_LIQUID_GLASS_BLUR_QUALITY, Renderer2D.LIQUID_GLASS_KAWASE_OFFSET_PX);
|
||||
}
|
||||
GpuTextureView blurView = blur.matchingView(sourceView, sourceSampler);
|
||||
GpuSampler blurSampler = blur.matchingSampler(sourceView, sourceSampler);
|
||||
if (blurView == null || blurSampler == null) {
|
||||
blurView = sourceView;
|
||||
blurSampler = sourceSampler;
|
||||
}
|
||||
if (blurView == ctx.mainColorView) return 0;
|
||||
|
||||
UIBatchUniforms.update(ctx.screenW, ctx.screenH);
|
||||
MeshRenderer builder = MeshRenderer.begin()
|
||||
.attachments(ctx.mainColorView, null)
|
||||
.pipeline(textBatch.pipeline)
|
||||
.mesh(textBatch.mesh)
|
||||
.uniform("UIBatch", UIBatchUniforms.get())
|
||||
.sampler("u_Texture", glyphTexture.getTextureView(), glyphTexture.getSampler())
|
||||
.sampler("u_SceneTexture", sourceView, sourceSampler)
|
||||
.sampler("u_BlurTexture", blurView, blurSampler);
|
||||
|
||||
if (isMsdf(textBatch.font)) {
|
||||
MsdfTextUniforms.update(textBatch.font.getPxRange(), textBatch.font.getAtlasWidth(), textBatch.font.getAtlasHeight());
|
||||
builder.uniform("MsdfText", MsdfTextUniforms.get());
|
||||
}
|
||||
|
||||
builder.end();
|
||||
return 1 + blurPassCalls;
|
||||
}
|
||||
|
||||
private static boolean isMsdf(@Nullable GlyphFont font) {
|
||||
return font != null && font.isMsdf();
|
||||
}
|
||||
}
|
||||
|
|
@ -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.render.engine.renderer.ui.batches.ordered.flush;
|
||||
|
||||
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.batches.DrawBatch;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.batches.TextBatch;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.batches.ordered.OrderedUiBatcher;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.batches.ordered.order.OrderState;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.blur.kawase.SharedKawaseBlur;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.deferred.DeferredOrderedSubmit;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.deferred.sched.UiDeferredScheduler;
|
||||
import dev.loki.lovisual.render.helpers.clip.ScissorFunction;
|
||||
|
||||
/**
|
||||
* Отложенная отправка order-списка: текущий батчер уходит в очередь deferred-слоя,
|
||||
* а в Renderer2D.UI_BATCHER подменяется свежим экземпляром из пула.
|
||||
*/
|
||||
public final class OrderedDeferral {
|
||||
private OrderedDeferral() {
|
||||
}
|
||||
|
||||
public static void defer(OrderedUiBatcher batcher, OrderState state, SharedKawaseBlur blur, boolean finish) {
|
||||
if (!state.hasPendingWork()) {
|
||||
Renderer2D.BATCH_STATS.noteEmpty(batcher.isActive(), state.poolTotal());
|
||||
if (finish) {
|
||||
blur.reset();
|
||||
batcher.deactivate();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
int batches = state.entryCount();
|
||||
int vertices = 0;
|
||||
int indices = 0;
|
||||
for (int i = 0; i < state.entries().size(); i++) {
|
||||
Object entry = state.entries().get(i);
|
||||
if (entry instanceof DrawBatch drawBatch) {
|
||||
if (drawBatch.mesh.isBuilding()) drawBatch.mesh.end();
|
||||
vertices += drawBatch.mesh.getVertexCount();
|
||||
indices += drawBatch.mesh.getIndicesCount();
|
||||
} else if (entry instanceof TextBatch textBatch && textBatch.mesh != null) {
|
||||
if (textBatch.mesh.isBuilding()) textBatch.mesh.end();
|
||||
vertices += textBatch.mesh.getVertexCount();
|
||||
indices += textBatch.mesh.getIndicesCount();
|
||||
}
|
||||
}
|
||||
|
||||
UiDeferredScheduler.enqueue(new DeferredOrderedSubmit(
|
||||
UiDeferredScheduler.layerForCurrentPhase(state.onlyItems()),
|
||||
UiDeferredScheduler.snapshotViewport(),
|
||||
ScissorFunction.currentFramebufferScissor(),
|
||||
batcher
|
||||
));
|
||||
|
||||
OrderedUiBatcher replacement = UiDeferredScheduler.obtainBatcher();
|
||||
replacement.prepareAsReplacement(!finish);
|
||||
Renderer2D.UI_BATCHER = replacement;
|
||||
|
||||
Renderer2D.BATCH_STATS.update(batcher.isActive(), batches, 0, vertices, indices, state.poolTotal());
|
||||
Renderer2D.BATCH_STATS.setActive(!finish);
|
||||
}
|
||||
}
|
||||
|
|
@ -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.render.engine.renderer.ui.batches.ordered.flush;
|
||||
|
||||
import com.mojang.blaze3d.textures.GpuSampler;
|
||||
import com.mojang.blaze3d.textures.GpuTextureView;
|
||||
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
|
||||
import dev.loki.lovisual.render.engine.renderer.MeshRenderer;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.batches.DrawBatch;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.batches.ordered.UiBatchType;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.batches.ordered.flush.OrderedFlushRenderer.Ctx;
|
||||
import dev.loki.lovisual.render.engine.uniform.impl.ui.MsdfTextUniforms;
|
||||
|
||||
/**
|
||||
* Пути отрисовки DrawBatch: прямой блюр-оверлей, direct liquid-glass,
|
||||
* offscreen fx-композит и обычная отрисовка. Соответствует прежнему
|
||||
* телу цикла flush() по эффектам 1:1.
|
||||
*/
|
||||
final class OrderedDrawBatchFlush {
|
||||
private OrderedDrawBatchFlush() {
|
||||
}
|
||||
|
||||
static void render(DrawBatch batch, Ctx ctx) {
|
||||
if (batch.mesh.isBuilding()) batch.mesh.end();
|
||||
if (batch.mesh.getIndicesCount() <= 0) return;
|
||||
|
||||
ctx.drawCalls++;
|
||||
ctx.vertices += batch.mesh.getVertexCount();
|
||||
ctx.indices += batch.mesh.getIndicesCount();
|
||||
|
||||
if (isFx(batch.type)) {
|
||||
int blurPassCalls = ctx.blur.prepare(ctx.mc, batch.view, batch.sampler, ctx.screenW, ctx.screenH, ctx.uiScale,
|
||||
batch.blurQuality, batch.blurOffsetPx);
|
||||
if (ctx.blur.blurredView() != null && ctx.blur.blurredSampler() != null) {
|
||||
MeshRenderer.begin()
|
||||
.attachments(ctx.mainColorView, null)
|
||||
.pipeline(batch.type.pipeline)
|
||||
.mesh(batch.mesh)
|
||||
.uniform("UIBatch", ctx.uiBatch())
|
||||
.sampler("u_Texture", ctx.blur.blurredView(), ctx.blur.blurredSampler())
|
||||
.end();
|
||||
ctx.drawCalls += blurPassCalls;
|
||||
return;
|
||||
}
|
||||
if (batch.view == ctx.mainColorView) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (batch.type == UiBatchType.LIQUID_GLASS) {
|
||||
renderLiquidGlass(batch, ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFx(batch.type)) {
|
||||
if (renderOffscreenFx(batch, ctx)) {
|
||||
return;
|
||||
}
|
||||
// Never sample from the main color attachment while writing to it.
|
||||
// If offscreen FX target is unavailable, skip this pass to avoid undefined behavior.
|
||||
if (batch.type.usesSampler
|
||||
&& batch.view == ctx.mainColorView
|
||||
&& !(batch.type == UiBatchType.LIQUID_GLASS && ctx.liquidSourceView != null)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (isSamplerOnMainGuarded(batch, ctx)) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderPlain(batch, ctx);
|
||||
}
|
||||
|
||||
private static void renderLiquidGlass(DrawBatch batch, Ctx ctx) {
|
||||
GpuTextureView sourceView = ctx.liquidSourceView != null ? ctx.liquidSourceView : batch.view;
|
||||
GpuSampler sourceSampler = ctx.liquidSourceSampler != null ? ctx.liquidSourceSampler : batch.sampler;
|
||||
boolean clippedComposite = batch.shapeClipActive;
|
||||
if (clippedComposite) {
|
||||
ctx.blur.adoptCached(sourceView, sourceSampler, ctx.screenW, ctx.screenH, ctx.uiScale,
|
||||
batch.blurQuality, batch.blurOffsetPx);
|
||||
} else {
|
||||
ctx.drawCalls += ctx.blur.prepare(ctx.mc, sourceView, sourceSampler, ctx.screenW, ctx.screenH, ctx.uiScale,
|
||||
batch.blurQuality, batch.blurOffsetPx);
|
||||
}
|
||||
GpuTextureView liquidBlurView = ctx.blur.matchingView(sourceView, sourceSampler);
|
||||
GpuSampler liquidBlurSampler = ctx.blur.matchingSampler(sourceView, sourceSampler);
|
||||
if (liquidBlurView == null || liquidBlurSampler == null) {
|
||||
liquidBlurView = sourceView;
|
||||
liquidBlurSampler = sourceSampler;
|
||||
}
|
||||
|
||||
if (sourceView == null || sourceSampler == null
|
||||
|| liquidBlurView == null || liquidBlurSampler == null
|
||||
|| sourceView == ctx.mainColorView
|
||||
|| liquidBlurView == ctx.mainColorView) {
|
||||
return;
|
||||
}
|
||||
|
||||
MeshRenderer.begin()
|
||||
.attachments(ctx.mainColorView, null)
|
||||
.pipeline(batch.type.pipeline)
|
||||
.mesh(batch.mesh)
|
||||
.uniform("UIBatch", ctx.uiBatch())
|
||||
.sampler("u_Texture", sourceView, sourceSampler)
|
||||
.sampler("u_BlurTexture", liquidBlurView, liquidBlurSampler)
|
||||
.end();
|
||||
}
|
||||
|
||||
private static boolean renderOffscreenFx(DrawBatch batch, Ctx ctx) {
|
||||
if (!ctx.ensureFx()) {
|
||||
return false;
|
||||
}
|
||||
MeshRenderer fxBuilder = MeshRenderer.begin()
|
||||
.attachments(ctx.fxBuffer)
|
||||
.clearColor(0x00000000)
|
||||
.pipeline(batch.type.pipeline)
|
||||
.mesh(batch.mesh);
|
||||
|
||||
if (batch.type.needsUiBatch) {
|
||||
fxBuilder.uniform("UIBatch", ctx.uiBatch());
|
||||
}
|
||||
if (batch.type.usesSampler) {
|
||||
GpuTextureView sourceView = batch.type == UiBatchType.LIQUID_GLASS && ctx.liquidSourceView != null ? ctx.liquidSourceView : batch.view;
|
||||
GpuSampler sourceSampler = batch.type == UiBatchType.LIQUID_GLASS && ctx.liquidSourceSampler != null ? ctx.liquidSourceSampler : batch.sampler;
|
||||
fxBuilder.sampler("u_Texture", sourceView, sourceSampler);
|
||||
}
|
||||
if (batch.type == UiBatchType.LIQUID_GLASS) {
|
||||
GpuTextureView liquidBlurView = ctx.blur.blurredView() != null ? ctx.blur.blurredView() : batch.view;
|
||||
GpuSampler liquidBlurSampler = ctx.blur.blurredSampler() != null ? ctx.blur.blurredSampler() : batch.sampler;
|
||||
if (liquidBlurView != null && liquidBlurSampler != null) {
|
||||
fxBuilder.sampler("u_BlurTexture", liquidBlurView, liquidBlurSampler);
|
||||
}
|
||||
}
|
||||
|
||||
fxBuilder.end();
|
||||
|
||||
MeshRenderer.begin()
|
||||
.attachments(ctx.mainColorView, null)
|
||||
.pipeline(LoVisualRenderPipelines.UI_TEXTURED)
|
||||
.mesh(ctx.fxCompositeMesh)
|
||||
.sampler("u_Texture", ctx.fxView, ctx.fxSampler)
|
||||
.end();
|
||||
|
||||
ctx.drawCalls++;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void renderPlain(DrawBatch batch, Ctx ctx) {
|
||||
MeshRenderer builder = MeshRenderer.begin()
|
||||
.attachments(ctx.mainColorView, null)
|
||||
.pipeline(batch.type.pipeline)
|
||||
.mesh(batch.mesh);
|
||||
|
||||
if (batch.type.needsUiBatch) {
|
||||
builder.uniform("UIBatch", ctx.uiBatch());
|
||||
}
|
||||
if (batch.type.usesSampler) {
|
||||
GpuTextureView sourceView = batch.type == UiBatchType.LIQUID_GLASS && ctx.liquidSourceView != null ? ctx.liquidSourceView : batch.view;
|
||||
GpuSampler sourceSampler = batch.type == UiBatchType.LIQUID_GLASS && ctx.liquidSourceSampler != null ? ctx.liquidSourceSampler : batch.sampler;
|
||||
builder.sampler("u_Texture", sourceView, sourceSampler);
|
||||
}
|
||||
if (batch.type == UiBatchType.SVG_MSDF) {
|
||||
MsdfTextUniforms.update(batch.msdfPxRange, batch.msdfAtlasWidth, batch.msdfAtlasHeight);
|
||||
builder.uniform("MsdfText", MsdfTextUniforms.get());
|
||||
}
|
||||
if (batch.type == UiBatchType.LIQUID_GLASS) {
|
||||
GpuTextureView liquidBlurView = ctx.blur.blurredView() != null ? ctx.blur.blurredView() : batch.view;
|
||||
GpuSampler liquidBlurSampler = ctx.blur.blurredSampler() != null ? ctx.blur.blurredSampler() : batch.sampler;
|
||||
if (liquidBlurView != null && liquidBlurSampler != null) {
|
||||
builder.sampler("u_BlurTexture", liquidBlurView, liquidBlurSampler);
|
||||
}
|
||||
}
|
||||
|
||||
builder.end();
|
||||
}
|
||||
|
||||
private static boolean isFx(UiBatchType type) {
|
||||
return type == UiBatchType.BLUR || type == UiBatchType.GLASS_BLUR || type == UiBatchType.BLUR_COMPLEX;
|
||||
}
|
||||
|
||||
private static boolean isSamplerOnMainGuarded(DrawBatch batch, Ctx ctx) {
|
||||
if (!isFx(batch.type) && batch.type != UiBatchType.LIQUID_GLASS) return false;
|
||||
return batch.type.usesSampler
|
||||
&& batch.view == ctx.mainColorView
|
||||
&& !(batch.type == UiBatchType.LIQUID_GLASS && ctx.liquidSourceView != null);
|
||||
}
|
||||
}
|
||||
|
|
@ -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.engine.renderer.ui.batches.ordered.flush;
|
||||
|
||||
import com.mojang.blaze3d.buffers.GpuBufferSlice;
|
||||
import com.mojang.blaze3d.pipeline.TextureTarget;
|
||||
import com.mojang.blaze3d.textures.GpuSampler;
|
||||
import com.mojang.blaze3d.textures.GpuTextureView;
|
||||
import dev.loki.lovisual.render.engine.postprocess.post.PostProcessManager;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.batches.DrawBatch;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.batches.ItemBatch;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.batches.TextBatch;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.batches.ordered.UiBatchType;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.batches.ordered.order.OrderState;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.blur.UiBlurResources;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.blur.kawase.SharedKawaseBlur;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.item.ItemBatchRenderer;
|
||||
import dev.loki.lovisual.render.engine.text.render.TextRenderSystem;
|
||||
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
|
||||
import dev.loki.lovisual.render.engine.uniform.impl.ui.UIBatchUniforms;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Один проход flush'а order-списка: диспетчеризация item/text/draw записей.
|
||||
* Мутабельное состояние прохода (счётчики, ленивые fx-таргеты) живёт в Ctx.
|
||||
*/
|
||||
public final class OrderedFlushRenderer {
|
||||
private OrderedFlushRenderer() {
|
||||
}
|
||||
|
||||
public record FlushResult(int drawCalls, int vertices, int indices) {
|
||||
}
|
||||
|
||||
public static FlushResult flush(OrderState state, SharedKawaseBlur blur, Minecraft mc,
|
||||
GpuTextureView mainColorView, float screenW, float screenH, float uiScale) {
|
||||
Ctx ctx = new Ctx(blur, mc, mainColorView, screenW, screenH, uiScale);
|
||||
if (hasLiquidGlass(state.entries())) {
|
||||
resolveLiquidSource(ctx);
|
||||
}
|
||||
for (Object entry : state.entries()) {
|
||||
if (entry instanceof ItemBatch itemBatch) {
|
||||
if (!itemBatch.isEmpty()) {
|
||||
ctx.drawCalls += ItemBatchRenderer.flush(itemBatch);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (entry instanceof TextBatch textBatch) {
|
||||
if (!textBatch.isEmpty()) {
|
||||
ctx.vertices += textBatch.mesh.getVertexCount();
|
||||
ctx.indices += textBatch.mesh.getIndicesCount();
|
||||
if (textBatch.liquidGlassText) {
|
||||
ctx.drawCalls += LiquidGlassTextFlush.flush(textBatch, ctx);
|
||||
} else {
|
||||
ctx.drawCalls++;
|
||||
TextRenderSystem.submitGlyphMeshImmediate(textBatch.label, textBatch.font, textBatch.mesh,
|
||||
textBatch.pipeline, textBatch.placement);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
OrderedDrawBatchFlush.render((DrawBatch) entry, ctx);
|
||||
}
|
||||
return new FlushResult(ctx.drawCalls, ctx.vertices, ctx.indices);
|
||||
}
|
||||
|
||||
private static boolean hasLiquidGlass(ObjectArrayList<Object> order) {
|
||||
for (Object orderedEntry : order) {
|
||||
if (orderedEntry instanceof DrawBatch orderedBatch && orderedBatch.type == UiBatchType.LIQUID_GLASS) {
|
||||
return true;
|
||||
}
|
||||
if (orderedEntry instanceof TextBatch orderedText && orderedText.liquidGlassText) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void resolveLiquidSource(Ctx ctx) {
|
||||
/*
|
||||
* The HUD/world glass source is captured before GUI extraction. Never perform a
|
||||
* fallback copy of the active main framebuffer from inside a batch flush: doing so
|
||||
* inserts a render-target transition in the middle of the HUD phase and can stall the
|
||||
* frame or temporarily discard already submitted lower layers.
|
||||
*/
|
||||
TextureTarget sourceBuffer = UiBlurResources.ensureGlassSource(ctx.mc);
|
||||
GpuSampler copySampler = PostProcessManager.getSampler();
|
||||
if (UiBlurResources.isWorldSourceReady() && sourceBuffer != null && copySampler != null) {
|
||||
GpuTextureView sourceTargetView = sourceBuffer.getColorTextureView();
|
||||
if (sourceTargetView != null && sourceTargetView != ctx.mainColorView) {
|
||||
ctx.liquidSourceView = sourceTargetView;
|
||||
ctx.liquidSourceSampler = copySampler;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Мутабельное состояние одного flush-прохода, общее для всех записей.
|
||||
*/
|
||||
static final class Ctx {
|
||||
final SharedKawaseBlur blur;
|
||||
final Minecraft mc;
|
||||
final GpuTextureView mainColorView;
|
||||
final float screenW;
|
||||
final float screenH;
|
||||
final float uiScale;
|
||||
@Nullable GpuTextureView liquidSourceView;
|
||||
@Nullable GpuSampler liquidSourceSampler;
|
||||
@Nullable GpuBufferSlice uiBatch;
|
||||
@Nullable TextureTarget fxBuffer;
|
||||
@Nullable GpuTextureView fxView;
|
||||
@Nullable GpuSampler fxSampler;
|
||||
@Nullable MeshBuilder fxCompositeMesh;
|
||||
int drawCalls;
|
||||
int vertices;
|
||||
int indices;
|
||||
|
||||
Ctx(SharedKawaseBlur blur, Minecraft mc, GpuTextureView mainColorView,
|
||||
float screenW, float screenH, float uiScale) {
|
||||
this.blur = blur;
|
||||
this.mc = mc;
|
||||
this.mainColorView = mainColorView;
|
||||
this.screenW = screenW;
|
||||
this.screenH = screenH;
|
||||
this.uiScale = uiScale;
|
||||
}
|
||||
|
||||
GpuBufferSlice uiBatch() {
|
||||
if (uiBatch == null) {
|
||||
UIBatchUniforms.update(screenW, screenH);
|
||||
uiBatch = UIBatchUniforms.get();
|
||||
}
|
||||
return uiBatch;
|
||||
}
|
||||
|
||||
boolean ensureFx() {
|
||||
if (fxBuffer != null) {
|
||||
return fxView != null && fxSampler != null && fxCompositeMesh != null;
|
||||
}
|
||||
fxBuffer = UiBlurResources.ensureEffects(mc);
|
||||
if (fxBuffer != null) {
|
||||
fxView = fxBuffer.getColorTextureView();
|
||||
fxSampler = PostProcessManager.getSampler();
|
||||
int compW = Math.max(1, Math.round(screenW / uiScale));
|
||||
int compH = Math.max(1, Math.round(screenH / uiScale));
|
||||
fxCompositeMesh = UiBlurResources.ensureCompositeMesh(compW, compH);
|
||||
}
|
||||
return fxBuffer != null && fxView != null && fxSampler != null && fxCompositeMesh != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.render.engine.renderer.ui.batches.ordered.order;
|
||||
|
||||
import com.mojang.blaze3d.textures.GpuSampler;
|
||||
import com.mojang.blaze3d.textures.GpuTextureView;
|
||||
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.batches.DrawBatch;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.batches.ordered.UiBatchType;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.blur.UiBlurResources;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.dispatch.UiRenderDispatcher;
|
||||
import dev.loki.lovisual.render.helpers.clip.ClipFunction;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
|
||||
/**
|
||||
* Пул DrawBatch (шейп/блюр/msdf) с переиспользованием по cursor'у и склейкой
|
||||
* соседних совместимых batch'ей. Порядок отрисовки задаёт общий order-список.
|
||||
*/
|
||||
final class DrawBatchPool {
|
||||
@SuppressWarnings("unchecked")
|
||||
private final ObjectArrayList<DrawBatch>[] pools = new ObjectArrayList[UiBatchType.values().length];
|
||||
private final int[] poolCursor = new int[UiBatchType.values().length];
|
||||
private final ObjectArrayList<Object> order;
|
||||
|
||||
DrawBatchPool(ObjectArrayList<Object> order) {
|
||||
this.order = order;
|
||||
for (int i = 0; i < pools.length; i++) {
|
||||
pools[i] = new ObjectArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
DrawBatch getOrCreate(UiBatchType type, GpuTextureView view, GpuSampler sampler) {
|
||||
boolean shapeClipActive = ClipFunction.isShapeClipActive();
|
||||
DrawBatch last = lastMergeable(type, view, sampler, shapeClipActive);
|
||||
if (last != null) return last;
|
||||
DrawBatch batch = obtain(type);
|
||||
batch.begin(view, sampler, shapeClipActive);
|
||||
order.add(batch);
|
||||
UiRenderDispatcher.recordBackendCommand(type);
|
||||
return batch;
|
||||
}
|
||||
|
||||
DrawBatch getOrCreateBlur(UiBatchType type, GpuTextureView view, GpuSampler sampler,
|
||||
Renderer2D.BlurQuality quality, float offsetPx) {
|
||||
if (type == UiBatchType.LIQUID_GLASS) {
|
||||
UiBlurResources.requestLiquidGlassBlur();
|
||||
}
|
||||
boolean shapeClipActive = ClipFunction.isShapeClipActive();
|
||||
Renderer2D.BlurQuality normalizedQuality = quality != null ? quality : Renderer2D.DEFAULT_BLUR_QUALITY;
|
||||
float normalizedOffset = Float.isFinite(offsetPx) ? Math.max(0.0f, offsetPx) : Renderer2D.DEFAULT_KAWASE_OFFSET_PX;
|
||||
DrawBatch last = lastMergeableBlur(type, view, sampler, normalizedQuality, normalizedOffset, shapeClipActive);
|
||||
if (last != null) return last;
|
||||
DrawBatch batch = obtain(type);
|
||||
batch.beginBlur(view, sampler, normalizedQuality, normalizedOffset, shapeClipActive);
|
||||
order.add(batch);
|
||||
UiRenderDispatcher.recordBackendCommand(type);
|
||||
return batch;
|
||||
}
|
||||
|
||||
DrawBatch getOrCreateMsdf(GpuTextureView view, GpuSampler sampler, float pxRange, int atlasWidth, int atlasHeight) {
|
||||
boolean shapeClipActive = ClipFunction.isShapeClipActive();
|
||||
DrawBatch last = lastMergeableMsdf(view, sampler, pxRange, atlasWidth, atlasHeight, shapeClipActive);
|
||||
if (last != null) return last;
|
||||
DrawBatch batch = obtain(UiBatchType.SVG_MSDF);
|
||||
batch.beginMsdf(view, sampler, pxRange, atlasWidth, atlasHeight, shapeClipActive);
|
||||
order.add(batch);
|
||||
UiRenderDispatcher.recordBackendCommand(UiBatchType.SVG_MSDF);
|
||||
return batch;
|
||||
}
|
||||
|
||||
private DrawBatch lastMergeable(UiBatchType type, GpuTextureView view, GpuSampler sampler, boolean shapeClipActive) {
|
||||
if (order.isEmpty()) return null;
|
||||
Object last = order.get(order.size() - 1);
|
||||
return last instanceof DrawBatch drawBatch && drawBatch.canMerge(type, view, sampler, shapeClipActive)
|
||||
? drawBatch
|
||||
: null;
|
||||
}
|
||||
|
||||
private DrawBatch lastMergeableBlur(UiBatchType type, GpuTextureView view, GpuSampler sampler,
|
||||
Renderer2D.BlurQuality quality, float offsetPx, boolean shapeClipActive) {
|
||||
if (order.isEmpty()) return null;
|
||||
Object last = order.get(order.size() - 1);
|
||||
return last instanceof DrawBatch drawBatch
|
||||
&& drawBatch.canMergeBlur(type, view, sampler, quality, offsetPx, shapeClipActive)
|
||||
? drawBatch
|
||||
: null;
|
||||
}
|
||||
|
||||
private DrawBatch lastMergeableMsdf(GpuTextureView view, GpuSampler sampler, float pxRange,
|
||||
int atlasWidth, int atlasHeight, boolean shapeClipActive) {
|
||||
if (order.isEmpty()) return null;
|
||||
Object last = order.get(order.size() - 1);
|
||||
return last instanceof DrawBatch drawBatch
|
||||
&& drawBatch.canMergeMsdf(view, sampler, pxRange, atlasWidth, atlasHeight, shapeClipActive)
|
||||
? drawBatch
|
||||
: null;
|
||||
}
|
||||
|
||||
private DrawBatch obtain(UiBatchType type) {
|
||||
int idx = type.ordinal();
|
||||
ObjectArrayList<DrawBatch> pool = pools[idx];
|
||||
int cursor = poolCursor[idx];
|
||||
DrawBatch batch;
|
||||
if (cursor < pool.size()) {
|
||||
batch = pool.get(cursor);
|
||||
} else {
|
||||
batch = new DrawBatch(type);
|
||||
pool.add(batch);
|
||||
}
|
||||
poolCursor[idx] = cursor + 1;
|
||||
return batch;
|
||||
}
|
||||
|
||||
void resetCursors() {
|
||||
for (int i = 0; i < poolCursor.length; i++) {
|
||||
poolCursor[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
int poolTotal() {
|
||||
int total = 0;
|
||||
for (ObjectArrayList<DrawBatch> pool : pools) {
|
||||
total += pool.size();
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
void closeRetained() {
|
||||
for (ObjectArrayList<DrawBatch> pool : pools) {
|
||||
for (int i = 0, size = pool.size(); i < size; i++) {
|
||||
DrawBatch batch = pool.get(i);
|
||||
if (batch != null && batch.mesh != null) {
|
||||
batch.mesh.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.render.engine.renderer.ui.batches.ordered.order;
|
||||
|
||||
import com.mojang.blaze3d.pipeline.RenderPipeline;
|
||||
import com.mojang.blaze3d.textures.GpuSampler;
|
||||
import com.mojang.blaze3d.textures.GpuTextureView;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.batches.ItemBatch;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.batches.TextBatch;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.blur.UiBlurResources;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.dispatch.UiRenderDispatcher;
|
||||
import dev.loki.lovisual.render.engine.text.backend.placement.TextPlacementMode;
|
||||
import dev.loki.lovisual.render.engine.text.glyph.GlyphFont;
|
||||
import dev.loki.lovisual.render.helpers.clip.ClipFunction;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.gui.navigation.ScreenRectangle;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Пулы item- и text-batch'ей. Текст обязан оставаться в точном порядке отрисовки:
|
||||
* склеиваются только соседние совместимые прогоны, текст никогда не переставляется
|
||||
* через другую запись order-списка (чат, nametags, слоёные HUD, marquee/clip-стеки).
|
||||
*/
|
||||
final class ItemTextPools {
|
||||
private final ObjectArrayList<ItemBatch> itemPool = new ObjectArrayList<>(32);
|
||||
private final ObjectArrayList<TextBatch> textPool = new ObjectArrayList<>(32);
|
||||
private int itemPoolCursor;
|
||||
private int textPoolCursor;
|
||||
private final ObjectArrayList<Object> order;
|
||||
|
||||
ItemTextPools(ObjectArrayList<Object> order) {
|
||||
this.order = order;
|
||||
}
|
||||
|
||||
ItemBatch getOrCreateItemBatch(@Nullable GuiGraphicsExtractor context, ItemStack stack) {
|
||||
ScreenRectangle scissor = context != null ? context.scissorStack.peek() : null;
|
||||
if (!order.isEmpty()) {
|
||||
Object last = order.get(order.size() - 1);
|
||||
if (last instanceof ItemBatch itemBatch && itemBatch.canMerge(context, scissor)) {
|
||||
return itemBatch;
|
||||
}
|
||||
}
|
||||
ItemBatch batch = obtainItemBatch();
|
||||
batch.begin(context, scissor);
|
||||
order.add(batch);
|
||||
UiRenderDispatcher.recordBackendCommand("ITEM");
|
||||
return batch;
|
||||
}
|
||||
|
||||
TextBatch getOrCreateTextBatch(String label, GlyphFont font, RenderPipeline pipeline,
|
||||
TextPlacementMode placement, boolean liquidGlassText) {
|
||||
if (liquidGlassText) {
|
||||
UiBlurResources.requestLiquidGlassBlur();
|
||||
}
|
||||
TextPlacementMode normalizedPlacement = placement != null ? placement : TextPlacementMode.UI;
|
||||
boolean shapeClipActive = ClipFunction.isShapeClipActive();
|
||||
if (!order.isEmpty()) {
|
||||
Object last = order.get(order.size() - 1);
|
||||
if (last instanceof TextBatch textBatch
|
||||
&& textBatch.canMerge(font, pipeline, normalizedPlacement, liquidGlassText, shapeClipActive)) {
|
||||
return textBatch;
|
||||
}
|
||||
}
|
||||
TextBatch batch = obtainTextBatch();
|
||||
batch.begin(label, font, pipeline, normalizedPlacement, liquidGlassText, shapeClipActive);
|
||||
order.add(batch);
|
||||
UiRenderDispatcher.recordBackendCommand(liquidGlassText ? "LIQUID_GLASS_TEXT" : "TEXT");
|
||||
return batch;
|
||||
}
|
||||
|
||||
private ItemBatch obtainItemBatch() {
|
||||
int cursor = itemPoolCursor;
|
||||
ItemBatch batch;
|
||||
if (cursor < itemPool.size()) {
|
||||
batch = itemPool.get(cursor);
|
||||
} else {
|
||||
batch = new ItemBatch();
|
||||
itemPool.add(batch);
|
||||
}
|
||||
itemPoolCursor = cursor + 1;
|
||||
return batch;
|
||||
}
|
||||
|
||||
private TextBatch obtainTextBatch() {
|
||||
int cursor = textPoolCursor;
|
||||
TextBatch batch;
|
||||
if (cursor < textPool.size()) {
|
||||
batch = textPool.get(cursor);
|
||||
} else {
|
||||
batch = new TextBatch();
|
||||
textPool.add(batch);
|
||||
}
|
||||
textPoolCursor = cursor + 1;
|
||||
return batch;
|
||||
}
|
||||
|
||||
void resetCursors() {
|
||||
itemPoolCursor = 0;
|
||||
textPoolCursor = 0;
|
||||
}
|
||||
|
||||
int poolTotal() {
|
||||
return itemPool.size() + textPool.size();
|
||||
}
|
||||
|
||||
void closeRetained() {
|
||||
for (int i = 0, size = textPool.size(); i < size; i++) {
|
||||
TextBatch batch = textPool.get(i);
|
||||
if (batch != null && batch.mesh != null) {
|
||||
batch.mesh.close();
|
||||
}
|
||||
}
|
||||
for (int i = 0, size = itemPool.size(); i < size; i++) {
|
||||
ItemBatch batch = itemPool.get(i);
|
||||
if (batch != null) {
|
||||
batch.commands.clear();
|
||||
batch.resolvedStates.clear();
|
||||
batch.context = null;
|
||||
batch.scissor = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* 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.batches.ordered.order;
|
||||
|
||||
import com.mojang.blaze3d.pipeline.RenderPipeline;
|
||||
import com.mojang.blaze3d.textures.GpuSampler;
|
||||
import com.mojang.blaze3d.textures.GpuTextureView;
|
||||
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.batches.DrawBatch;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.batches.ItemBatch;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.batches.TextBatch;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.batches.ordered.UiBatchType;
|
||||
import dev.loki.lovisual.render.engine.text.backend.placement.TextPlacementMode;
|
||||
import dev.loki.lovisual.render.engine.text.glyph.GlyphFont;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Order-состояние батчера: общий список записей отрисовки и пулы batch'ей
|
||||
* с переиспользованием. Единственный владелец порядка; flush/deferral только
|
||||
* читают entries().
|
||||
*/
|
||||
public final class OrderState {
|
||||
private final ObjectArrayList<Object> order = new ObjectArrayList<>(128);
|
||||
private final DrawBatchPool drawPools = new DrawBatchPool(order);
|
||||
private final ItemTextPools itemTextPools = new ItemTextPools(order);
|
||||
|
||||
public boolean hasPendingWork() {
|
||||
return !order.isEmpty();
|
||||
}
|
||||
|
||||
public int entryCount() {
|
||||
return order.size();
|
||||
}
|
||||
|
||||
public ObjectArrayList<Object> entries() {
|
||||
return order;
|
||||
}
|
||||
|
||||
public boolean onlyItems() {
|
||||
if (order.isEmpty()) return false;
|
||||
for (int i = 0, size = order.size(); i < size; i++) {
|
||||
if (!(order.get(i) instanceof ItemBatch)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public DrawBatch getOrCreate(UiBatchType type, GpuTextureView view, GpuSampler sampler) {
|
||||
return drawPools.getOrCreate(type, view, sampler);
|
||||
}
|
||||
|
||||
public DrawBatch getOrCreateBlur(UiBatchType type, GpuTextureView view, GpuSampler sampler,
|
||||
Renderer2D.BlurQuality quality, float offsetPx) {
|
||||
return drawPools.getOrCreateBlur(type, view, sampler, quality, offsetPx);
|
||||
}
|
||||
|
||||
public DrawBatch getOrCreateMsdf(GpuTextureView view, GpuSampler sampler, float pxRange, int atlasWidth, int atlasHeight) {
|
||||
return drawPools.getOrCreateMsdf(view, sampler, pxRange, atlasWidth, atlasHeight);
|
||||
}
|
||||
|
||||
public ItemBatch getOrCreateItemBatch(@Nullable GuiGraphicsExtractor context, ItemStack stack) {
|
||||
return itemTextPools.getOrCreateItemBatch(context, stack);
|
||||
}
|
||||
|
||||
public TextBatch getOrCreateTextBatch(String label, GlyphFont font, RenderPipeline pipeline,
|
||||
TextPlacementMode placement, boolean liquidGlassText) {
|
||||
return itemTextPools.getOrCreateTextBatch(label, font, pipeline, placement, liquidGlassText);
|
||||
}
|
||||
|
||||
public void resetOrder() {
|
||||
order.clear();
|
||||
drawPools.resetCursors();
|
||||
itemTextPools.resetCursors();
|
||||
}
|
||||
|
||||
public int poolTotal() {
|
||||
return drawPools.poolTotal() + itemTextPools.poolTotal();
|
||||
}
|
||||
|
||||
public void closeRetained() {
|
||||
drawPools.closeRetained();
|
||||
itemTextPools.closeRetained();
|
||||
}
|
||||
}
|
||||
|
|
@ -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.render.engine.renderer.ui.blur.kawase;
|
||||
|
||||
import com.mojang.blaze3d.pipeline.TextureTarget;
|
||||
import com.mojang.blaze3d.textures.GpuSampler;
|
||||
import com.mojang.blaze3d.textures.GpuTextureView;
|
||||
import dev.loki.lovisual.render.engine.core.frame.RenderPhase;
|
||||
import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem;
|
||||
import dev.loki.lovisual.render.engine.postprocess.post.PostProcessManager;
|
||||
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.blur.FrameBlurCacheEntry;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.blur.UiBlurResources;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Исполнение Kawase-пассов (down/up-цепочка) и наполнение кэша UiBlurResources.
|
||||
* Пишет результат в переданное {@link SharedKawaseBlur}-состояние.
|
||||
*/
|
||||
final class KawaseBlurPreparer {
|
||||
private KawaseBlurPreparer() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Снимок текущей позиции в цепочке пассов.
|
||||
*/
|
||||
private record PassState(GpuTextureView view, GpuSampler sampler, int w, int h, int drawCalls) {
|
||||
}
|
||||
|
||||
static int prepare(SharedKawaseBlur blur,
|
||||
Minecraft mc,
|
||||
@Nullable GpuTextureView sourceView,
|
||||
@Nullable GpuSampler sourceSampler,
|
||||
float screenW,
|
||||
float screenH,
|
||||
float uiScale,
|
||||
Renderer2D.BlurQuality blurQuality,
|
||||
float offsetPx) {
|
||||
if (LoVisualRenderSystem.rhi().shapeClip().isActive()) {
|
||||
blur.adoptCached(sourceView, sourceSampler, screenW, screenH, uiScale, blurQuality, offsetPx);
|
||||
return 0;
|
||||
}
|
||||
return prepareInternal(blur, mc, sourceView, sourceSampler, screenW, screenH, uiScale, blurQuality, offsetPx);
|
||||
}
|
||||
|
||||
static void prewarm(Minecraft mc,
|
||||
@Nullable GpuTextureView sourceView,
|
||||
@Nullable GpuSampler sourceSampler,
|
||||
float screenW,
|
||||
float screenH,
|
||||
float uiScale) {
|
||||
if (mc == null || sourceView == null || sourceSampler == null) return;
|
||||
prepareInternal(new SharedKawaseBlur(), mc, sourceView, sourceSampler, screenW, screenH, uiScale,
|
||||
Renderer2D.DEFAULT_LIQUID_GLASS_BLUR_QUALITY, Renderer2D.LIQUID_GLASS_KAWASE_OFFSET_PX);
|
||||
}
|
||||
|
||||
private static int prepareInternal(SharedKawaseBlur blur,
|
||||
Minecraft mc,
|
||||
@Nullable GpuTextureView sourceView,
|
||||
@Nullable GpuSampler sourceSampler,
|
||||
float screenW,
|
||||
float screenH,
|
||||
float uiScale,
|
||||
Renderer2D.BlurQuality blurQuality,
|
||||
float offsetPx) {
|
||||
if (sourceView == null || sourceSampler == null) {
|
||||
return 0;
|
||||
}
|
||||
Renderer2D.BlurQuality quality = blurQuality != null ? blurQuality : Renderer2D.DEFAULT_BLUR_QUALITY;
|
||||
int iterations = Math.max(1, quality.iterations);
|
||||
float passOffset = Float.isFinite(offsetPx) ? Math.max(0.0f, offsetPx) : Renderer2D.DEFAULT_KAWASE_OFFSET_PX;
|
||||
|
||||
if (blur.blurredView != null && blur.blurredSampler != null
|
||||
&& blur.sourceView == sourceView
|
||||
&& blur.sourceSampler == sourceSampler
|
||||
&& blur.quality == quality
|
||||
&& Float.compare(blur.offsetPx, passOffset) == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
long frameId = UiBlurResources.currentFrameId();
|
||||
RenderPhase phase = UiBlurResources.currentPhase();
|
||||
FrameBlurCacheEntry cachedBlur = UiBlurResources.findReusable(frameId, phase, sourceView, sourceSampler,
|
||||
screenW, screenH, uiScale, quality, passOffset);
|
||||
if (cachedBlur != null) {
|
||||
blur.sourceView = sourceView;
|
||||
blur.sourceSampler = sourceSampler;
|
||||
blur.blurredView = cachedBlur.blurredView;
|
||||
blur.blurredSampler = cachedBlur.blurredSampler;
|
||||
blur.quality = quality;
|
||||
blur.offsetPx = passOffset;
|
||||
return 0;
|
||||
}
|
||||
|
||||
blur.reset();
|
||||
|
||||
GpuSampler blurSampler = PostProcessManager.getSampler();
|
||||
if (blurSampler == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
PassState pass = new PassState(sourceView, sourceSampler,
|
||||
Math.max(1, Math.round(screenW)), Math.max(1, Math.round(screenH)), 0);
|
||||
for (int level = 0; level < iterations; level++) {
|
||||
pass = runLevel(mc, true, level, pass, blurSampler, passOffset);
|
||||
if (pass == null || pass.view() == null) return 0;
|
||||
}
|
||||
for (int level = iterations - 2; level >= 0; level--) {
|
||||
pass = runLevel(mc, false, level, pass, blurSampler, passOffset);
|
||||
if (pass == null || pass.view() == null) return 0;
|
||||
}
|
||||
|
||||
blur.sourceView = sourceView;
|
||||
blur.sourceSampler = sourceSampler;
|
||||
blur.blurredView = pass.view();
|
||||
blur.blurredSampler = blurSampler;
|
||||
blur.quality = quality;
|
||||
blur.offsetPx = passOffset;
|
||||
UiBlurResources.remember(frameId, phase, sourceView, sourceSampler, pass.view(), blurSampler,
|
||||
screenW, screenH, uiScale, quality, passOffset);
|
||||
return pass.drawCalls();
|
||||
}
|
||||
|
||||
/**
|
||||
* Один пасс; при неудаче цепочки возвращает null (ранний выход без результата).
|
||||
*/
|
||||
private static @Nullable PassState runLevel(Minecraft mc, boolean down, int level, PassState in,
|
||||
GpuSampler blurSampler, float passOffset) {
|
||||
TextureTarget target = down
|
||||
? UiBlurResources.ensureKawaseDown(mc, level)
|
||||
: UiBlurResources.ensureKawaseUp(mc, level);
|
||||
if (target == null
|
||||
|| !KawasePass.draw(target, in.view(), in.sampler(), in.w(), in.h(), passOffset, !down)) {
|
||||
return null;
|
||||
}
|
||||
return new PassState(target.getColorTextureView(), blurSampler,
|
||||
Math.max(1, target.width), Math.max(1, target.height), in.drawCalls() + 1);
|
||||
}
|
||||
}
|
||||
|
|
@ -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.render.engine.renderer.ui.blur.kawase;
|
||||
|
||||
import com.mojang.blaze3d.pipeline.RenderTarget;
|
||||
import com.mojang.blaze3d.pipeline.TextureTarget;
|
||||
import com.mojang.blaze3d.textures.GpuSampler;
|
||||
import com.mojang.blaze3d.textures.GpuTextureView;
|
||||
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
|
||||
import dev.loki.lovisual.render.engine.renderer.MeshRenderer;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.blur.UiBlurResources;
|
||||
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
|
||||
import dev.loki.lovisual.render.engine.uniform.impl.ui.UIBlurUniforms;
|
||||
import net.minecraft.client.renderer.Projection;
|
||||
import org.joml.Matrix4f;
|
||||
|
||||
/**
|
||||
* Один проход Kawase-блюра (down/up) в offscreen-таргет.
|
||||
*/
|
||||
final class KawasePass {
|
||||
private KawasePass() {
|
||||
}
|
||||
|
||||
static boolean draw(TextureTarget target,
|
||||
GpuTextureView sourceView,
|
||||
GpuSampler sourceSampler,
|
||||
int sourceW,
|
||||
int sourceH,
|
||||
float passOffset,
|
||||
boolean upPass) {
|
||||
if (target == null || sourceView == null || sourceSampler == null) return false;
|
||||
MeshBuilder compositeMesh = UiBlurResources.ensureCompositeMesh(target.width, target.height);
|
||||
if (compositeMesh == null) return false;
|
||||
|
||||
Matrix4f previousProjection = MeshRenderer.projection();
|
||||
try {
|
||||
MeshRenderer.setProjection(orthoProjection(target.width, target.height));
|
||||
UIBlurUniforms.update(sourceW, sourceH, target.width, target.height,
|
||||
0f, 0f, passOffset, upPass ? 1.0f : 0.0f, 1.0f, 1.0f, 0xFFFFFF);
|
||||
MeshRenderer.begin()
|
||||
.attachments(target)
|
||||
.clearColor(0x00000000)
|
||||
.pipeline(LoVisualRenderPipelines.UI_BLUR)
|
||||
.mesh(compositeMesh)
|
||||
.uniform("UIBlur", UIBlurUniforms.get())
|
||||
.sampler("u_Texture", sourceView, sourceSampler)
|
||||
.end();
|
||||
return target.getColorTextureView() != null;
|
||||
} finally {
|
||||
MeshRenderer.setProjection(previousProjection);
|
||||
}
|
||||
}
|
||||
|
||||
private static Matrix4f orthoProjection(float width, float height) {
|
||||
Projection projection = new Projection();
|
||||
projection.setupOrtho(-1000.0f, 1000.0f, Math.max(1.0f, width), Math.max(1.0f, height), true);
|
||||
return projection.getMatrix(new Matrix4f());
|
||||
}
|
||||
}
|
||||
|
|
@ -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.render.engine.renderer.ui.blur.kawase;
|
||||
|
||||
import com.mojang.blaze3d.textures.GpuSampler;
|
||||
import com.mojang.blaze3d.textures.GpuTextureView;
|
||||
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.blur.FrameBlurCacheEntry;
|
||||
import dev.loki.lovisual.render.engine.renderer.ui.blur.UiBlurResources;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Общее состояние разделяемого Kawase-блюра на кадр/фазу: один источник — один
|
||||
* размытый результат, переиспользуемый всеми batch'ами с одинаковыми параметрами.
|
||||
* Пассы исполняет {@link KawaseBlurPreparer}.
|
||||
*/
|
||||
public final class SharedKawaseBlur {
|
||||
@Nullable GpuTextureView sourceView;
|
||||
@Nullable GpuSampler sourceSampler;
|
||||
@Nullable GpuTextureView blurredView;
|
||||
@Nullable GpuSampler blurredSampler;
|
||||
Renderer2D.BlurQuality quality = Renderer2D.DEFAULT_BLUR_QUALITY;
|
||||
float offsetPx = Renderer2D.DEFAULT_KAWASE_OFFSET_PX;
|
||||
|
||||
public void reset() {
|
||||
sourceView = null;
|
||||
sourceSampler = null;
|
||||
blurredView = null;
|
||||
blurredSampler = null;
|
||||
quality = Renderer2D.DEFAULT_BLUR_QUALITY;
|
||||
offsetPx = Renderer2D.DEFAULT_KAWASE_OFFSET_PX;
|
||||
}
|
||||
|
||||
public @Nullable GpuTextureView blurredView() {
|
||||
return blurredView;
|
||||
}
|
||||
|
||||
public @Nullable GpuSampler blurredSampler() {
|
||||
return blurredSampler;
|
||||
}
|
||||
|
||||
public @Nullable GpuTextureView matchingView(@Nullable GpuTextureView sourceView, @Nullable GpuSampler sourceSampler) {
|
||||
if (sourceView == null || sourceSampler == null) return null;
|
||||
if (blurredView == null || blurredSampler == null) return null;
|
||||
if (this.sourceView != sourceView || this.sourceSampler != sourceSampler) return null;
|
||||
return blurredView;
|
||||
}
|
||||
|
||||
public @Nullable GpuSampler matchingSampler(@Nullable GpuTextureView sourceView, @Nullable GpuSampler sourceSampler) {
|
||||
if (matchingView(sourceView, sourceSampler) == null) return null;
|
||||
return blurredSampler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Подхватывает уже посчитанный в этом кадре/фазе блюр (кэш UiBlurResources) —
|
||||
* используется при активном shape-clip, где новый проход рисовать нельзя.
|
||||
*/
|
||||
public boolean adoptCached(@Nullable GpuTextureView sourceView,
|
||||
@Nullable GpuSampler sourceSampler,
|
||||
float screenW,
|
||||
float screenH,
|
||||
float uiScale,
|
||||
Renderer2D.BlurQuality quality,
|
||||
float offsetPx) {
|
||||
if (sourceView == null || sourceSampler == null) return false;
|
||||
FrameBlurCacheEntry cachedBlur = UiBlurResources.findReusable(
|
||||
UiBlurResources.currentFrameId(),
|
||||
UiBlurResources.currentPhase(),
|
||||
sourceView,
|
||||
sourceSampler,
|
||||
screenW,
|
||||
screenH,
|
||||
uiScale,
|
||||
quality,
|
||||
offsetPx
|
||||
);
|
||||
if (cachedBlur == null || cachedBlur.blurredView == null || cachedBlur.blurredSampler == null) {
|
||||
return false;
|
||||
}
|
||||
this.sourceView = sourceView;
|
||||
this.sourceSampler = sourceSampler;
|
||||
blurredView = cachedBlur.blurredView;
|
||||
blurredSampler = cachedBlur.blurredSampler;
|
||||
this.quality = quality != null ? quality : Renderer2D.DEFAULT_BLUR_QUALITY;
|
||||
this.offsetPx = Float.isFinite(offsetPx) ? Math.max(0.0f, offsetPx) : Renderer2D.DEFAULT_KAWASE_OFFSET_PX;
|
||||
return true;
|
||||
}
|
||||
|
||||
public int prepare(Minecraft mc,
|
||||
@Nullable GpuTextureView sourceView,
|
||||
@Nullable GpuSampler sourceSampler,
|
||||
float screenW,
|
||||
float screenH,
|
||||
float uiScale,
|
||||
Renderer2D.BlurQuality blurQuality,
|
||||
float offsetPx) {
|
||||
return KawaseBlurPreparer.prepare(this, mc, sourceView, sourceSampler, screenW, screenH, uiScale, blurQuality, offsetPx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Прогрев liquid-glass блюра вне кадра: пассы исполняются сразу, результат
|
||||
* оседает в кэше UiBlurResources, собственное состояние не сохраняется.
|
||||
*/
|
||||
public static void prewarmLiquidGlassBlur(Minecraft mc,
|
||||
@Nullable GpuTextureView sourceView,
|
||||
@Nullable GpuSampler sourceSampler,
|
||||
float screenW,
|
||||
float screenH,
|
||||
float uiScale) {
|
||||
KawaseBlurPreparer.prewarm(mc, sourceView, sourceSampler, screenW, screenH, uiScale);
|
||||
}
|
||||
}
|
||||
|
|
@ -46,23 +46,23 @@ import dev.loki.lovisual.render.engine.svg.parser.SvgPathParser;
|
|||
public enum SvgMeshBackend {
|
||||
;
|
||||
private static final int MAX_CACHE = 96;
|
||||
private static final int MAX_TEXTURE_CACHE = 256;
|
||||
private static final int MAX_GEOMETRY_CACHE = 256;
|
||||
private static final int MAX_TEXTURE_SIDE = 4096;
|
||||
private static final int MAX_TEXTURE_PIXELS = 4 * 1024 * 1024;
|
||||
private static final int SMALL_TEXTURE_QUANTUM = 8;
|
||||
private static final int MEDIUM_TEXTURE_QUANTUM = 16;
|
||||
private static final int LARGE_TEXTURE_QUANTUM = 32;
|
||||
private static final double EPS = 1e-7;
|
||||
private static final int MAX_TRACE_KEYS = 512;
|
||||
private static final Set<String> TRACE_ONCE_KEYS = new HashSet<>();
|
||||
private static final Map<String, SvgDoc> CACHE = new LinkedHashMap<>(64, 0.75f, true) {
|
||||
static final int MAX_TEXTURE_CACHE = 256;
|
||||
static final int MAX_GEOMETRY_CACHE = 256;
|
||||
static final int MAX_TEXTURE_SIDE = 4096;
|
||||
static final int MAX_TEXTURE_PIXELS = 4 * 1024 * 1024;
|
||||
static final int SMALL_TEXTURE_QUANTUM = 8;
|
||||
static final int MEDIUM_TEXTURE_QUANTUM = 16;
|
||||
static final int LARGE_TEXTURE_QUANTUM = 32;
|
||||
static final double EPS = 1e-7;
|
||||
static final int MAX_TRACE_KEYS = 512;
|
||||
static final Set<String> TRACE_ONCE_KEYS = new HashSet<>();
|
||||
static final Map<String, SvgDoc> CACHE = new LinkedHashMap<>(64, 0.75f, true) {
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry<String, SvgDoc> eldest) {
|
||||
return size() > MAX_CACHE;
|
||||
}
|
||||
};
|
||||
private static final Map<GeometryKey, CompiledGeometry> GEOMETRY_CACHE = new LinkedHashMap<>(128, 0.75f, true) {
|
||||
static final Map<GeometryKey, CompiledGeometry> GEOMETRY_CACHE = new LinkedHashMap<>(128, 0.75f, true) {
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry<GeometryKey, CompiledGeometry> eldest) {
|
||||
if (size() > MAX_GEOMETRY_CACHE) {
|
||||
|
|
@ -72,7 +72,7 @@ public enum SvgMeshBackend {
|
|||
return false;
|
||||
}
|
||||
};
|
||||
private static final Map<TextureKey, CompiledTexture> TEXTURE_CACHE = new LinkedHashMap<>(128, 0.75f, true) {
|
||||
static final Map<TextureKey, CompiledTexture> TEXTURE_CACHE = new LinkedHashMap<>(128, 0.75f, true) {
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry<TextureKey, CompiledTexture> eldest) {
|
||||
if (size() > MAX_TEXTURE_CACHE) {
|
||||
|
|
@ -84,24 +84,7 @@ public enum SvgMeshBackend {
|
|||
};
|
||||
|
||||
public static void clearCache() {
|
||||
synchronized (CACHE) {
|
||||
CACHE.clear();
|
||||
}
|
||||
synchronized (TEXTURE_CACHE) {
|
||||
for (CompiledTexture texture : TEXTURE_CACHE.values()) {
|
||||
texture.close();
|
||||
}
|
||||
TEXTURE_CACHE.clear();
|
||||
}
|
||||
synchronized (GEOMETRY_CACHE) {
|
||||
for (CompiledGeometry geometry : GEOMETRY_CACHE.values()) {
|
||||
geometry.close();
|
||||
}
|
||||
GEOMETRY_CACHE.clear();
|
||||
}
|
||||
synchronized (TRACE_ONCE_KEYS) {
|
||||
TRACE_ONCE_KEYS.clear();
|
||||
}
|
||||
SvgCache.clearAll();
|
||||
}
|
||||
|
||||
public static void draw(Renderer2D renderer, Identifier svg, double x, double y, double w, double h) {
|
||||
|
|
@ -139,7 +122,7 @@ public enum SvgMeshBackend {
|
|||
var opt = mc.getResourceManager().getResource(id);
|
||||
if (opt.isEmpty()) return null;
|
||||
try (InputStream in = opt.get().open()) {
|
||||
SvgDoc parsed = parseSvg(in, key);
|
||||
SvgDoc parsed = SvgParser.parseSvg(in, key);
|
||||
if (parsed == null) return null;
|
||||
synchronized (CACHE) {
|
||||
CACHE.put(key, parsed);
|
||||
|
|
@ -158,7 +141,7 @@ public enum SvgMeshBackend {
|
|||
if (cached != null) return cached;
|
||||
}
|
||||
try (InputStream in = Files.newInputStream(path)) {
|
||||
SvgDoc parsed = parseSvg(in, key);
|
||||
SvgDoc parsed = SvgParser.parseSvg(in, key);
|
||||
if (parsed == null) return null;
|
||||
synchronized (CACHE) {
|
||||
CACHE.put(key, parsed);
|
||||
|
|
@ -170,763 +153,27 @@ public enum SvgMeshBackend {
|
|||
}
|
||||
}
|
||||
|
||||
private static @Nullable SvgDoc parseSvg(InputStream in, String cacheKey) {
|
||||
try {
|
||||
DocumentBuilderFactory f = DocumentBuilderFactory.newInstance();
|
||||
f.setNamespaceAware(false);
|
||||
f.setXIncludeAware(false);
|
||||
f.setExpandEntityReferences(false);
|
||||
f.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
|
||||
safeSet(f, "http://apache.org/xml/features/disallow-doctype-decl", true);
|
||||
safeSet(f, "http://xml.org/sax/features/external-general-entities", false);
|
||||
safeSet(f, "http://xml.org/sax/features/external-parameter-entities", false);
|
||||
safeSet(f, "http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
|
||||
DocumentBuilder b = f.newDocumentBuilder();
|
||||
Document doc = b.parse(in);
|
||||
Element root = doc.getDocumentElement();
|
||||
if (root == null) return null;
|
||||
|
||||
double[] vb = parseViewBox(root);
|
||||
SvgStyle rootStyle = SvgStyle.root();
|
||||
ArrayList<SvgShape> shapes = new ArrayList<>();
|
||||
walk(root, new AffineTransform(), rootStyle, shapes);
|
||||
return new SvgDoc(cacheKey, vb[0], vb[1], vb[2], vb[3], shapes);
|
||||
} catch (Exception e) {
|
||||
DebugLog.warn("[LoVisual][SVG] Parse failed: %s", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
private static void render(Renderer2D renderer, SvgDoc doc, double x, double y, double width, double height, SvgRenderOptions options) {
|
||||
SvgRender.render(renderer, doc, x, y, width, height, options);
|
||||
}
|
||||
|
||||
private static void safeSet(DocumentBuilderFactory f, String name, boolean value) {
|
||||
try {
|
||||
f.setFeature(name, value);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private static double[] parseViewBox(Element root) {
|
||||
String viewBox = root.getAttribute("viewBox");
|
||||
if (viewBox != null && !viewBox.isBlank()) {
|
||||
double[] vals = SvgStyleUtil.parseNumberList(viewBox);
|
||||
if (vals.length == 4 && vals[2] > 0.0 && vals[3] > 0.0) {
|
||||
return vals;
|
||||
}
|
||||
}
|
||||
double w = SvgStyleUtil.parseLength(root.getAttribute("width"), 24.0);
|
||||
double h = SvgStyleUtil.parseLength(root.getAttribute("height"), 24.0);
|
||||
if (w <= 0.0) w = 24.0;
|
||||
if (h <= 0.0) h = 24.0;
|
||||
return new double[]{0.0, 0.0, w, h};
|
||||
}
|
||||
|
||||
private static void walk(Element node,
|
||||
AffineTransform parentTransform,
|
||||
SvgStyle parentStyle,
|
||||
List<SvgShape> out) {
|
||||
String tag = node.getTagName();
|
||||
if (tag == null || tag.isBlank()) return;
|
||||
tag = tag.toLowerCase(Locale.ROOT);
|
||||
|
||||
SvgStyle style = parentStyle.copy();
|
||||
SvgStyleUtil.applyElementStyle(style, node);
|
||||
if (!style.visible) return;
|
||||
|
||||
AffineTransform transform = new AffineTransform(parentTransform);
|
||||
transform.concatenate(SvgStyleUtil.parseTransform(node.getAttribute("transform")));
|
||||
|
||||
switch (tag) {
|
||||
case "svg", "g", "a", "symbol" -> {
|
||||
NodeList children = node.getChildNodes();
|
||||
for (int i = 0; i < children.getLength(); i++) {
|
||||
Node child = children.item(i);
|
||||
if (child instanceof Element childElement) {
|
||||
walk(childElement, transform, style, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
case "defs" -> {
|
||||
}
|
||||
default -> {
|
||||
Path2D.Double path = buildPath(node, tag);
|
||||
if (path == null) return;
|
||||
int winding = "evenodd".equalsIgnoreCase(style.fillRule) ? Path2D.WIND_EVEN_ODD : Path2D.WIND_NON_ZERO;
|
||||
path.setWindingRule(winding);
|
||||
out.add(new SvgShape(path, transform, style.copy(), winding));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static @Nullable Path2D.Double buildPath(Element element, String tag) {
|
||||
return switch (tag) {
|
||||
case "path" -> {
|
||||
String d = element.getAttribute("d");
|
||||
if (d == null || d.isBlank()) yield null;
|
||||
yield SvgPathParser.parse(d);
|
||||
}
|
||||
case "rect" -> buildRect(element);
|
||||
case "circle" -> buildCircle(element);
|
||||
case "ellipse" -> buildEllipse(element);
|
||||
case "line" -> buildLine(element);
|
||||
case "polyline" -> buildPoly(element, false);
|
||||
case "polygon" -> buildPoly(element, true);
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
private static @Nullable Path2D.Double buildRect(Element e) {
|
||||
double x = SvgStyleUtil.parseLength(e.getAttribute("x"), 0.0);
|
||||
double y = SvgStyleUtil.parseLength(e.getAttribute("y"), 0.0);
|
||||
double w = SvgStyleUtil.parseLength(e.getAttribute("width"), 0.0);
|
||||
double h = SvgStyleUtil.parseLength(e.getAttribute("height"), 0.0);
|
||||
if (w <= 0.0 || h <= 0.0) return null;
|
||||
double rx = SvgStyleUtil.parseLength(e.getAttribute("rx"), -1.0);
|
||||
double ry = SvgStyleUtil.parseLength(e.getAttribute("ry"), -1.0);
|
||||
if (rx < 0.0 && ry >= 0.0) rx = ry;
|
||||
if (ry < 0.0 && rx >= 0.0) ry = rx;
|
||||
if (rx < 0.0) rx = 0.0;
|
||||
if (ry < 0.0) ry = 0.0;
|
||||
rx = Math.min(rx, w * 0.5);
|
||||
ry = Math.min(ry, h * 0.5);
|
||||
Path2D.Double out = new Path2D.Double();
|
||||
if (rx > 0.0 || ry > 0.0) out.append(new RoundRectangle2D.Double(x, y, w, h, rx * 2.0, ry * 2.0), false);
|
||||
else {
|
||||
out.moveTo(x, y);
|
||||
out.lineTo(x + w, y);
|
||||
out.lineTo(x + w, y + h);
|
||||
out.lineTo(x, y + h);
|
||||
out.closePath();
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static @Nullable Path2D.Double buildCircle(Element e) {
|
||||
double cx = SvgStyleUtil.parseLength(e.getAttribute("cx"), 0.0);
|
||||
double cy = SvgStyleUtil.parseLength(e.getAttribute("cy"), 0.0);
|
||||
double r = SvgStyleUtil.parseLength(e.getAttribute("r"), 0.0);
|
||||
if (r <= 0.0) return null;
|
||||
Path2D.Double out = new Path2D.Double();
|
||||
out.append(new Ellipse2D.Double(cx - r, cy - r, 2.0 * r, 2.0 * r), false);
|
||||
return out;
|
||||
}
|
||||
|
||||
private static @Nullable Path2D.Double buildEllipse(Element e) {
|
||||
double cx = SvgStyleUtil.parseLength(e.getAttribute("cx"), 0.0);
|
||||
double cy = SvgStyleUtil.parseLength(e.getAttribute("cy"), 0.0);
|
||||
double rx = SvgStyleUtil.parseLength(e.getAttribute("rx"), 0.0);
|
||||
double ry = SvgStyleUtil.parseLength(e.getAttribute("ry"), 0.0);
|
||||
if (rx <= 0.0 || ry <= 0.0) return null;
|
||||
Path2D.Double out = new Path2D.Double();
|
||||
out.append(new Ellipse2D.Double(cx - rx, cy - ry, 2.0 * rx, 2.0 * ry), false);
|
||||
return out;
|
||||
}
|
||||
|
||||
private static Path2D.Double buildLine(Element e) {
|
||||
double x1 = SvgStyleUtil.parseLength(e.getAttribute("x1"), 0.0);
|
||||
double y1 = SvgStyleUtil.parseLength(e.getAttribute("y1"), 0.0);
|
||||
double x2 = SvgStyleUtil.parseLength(e.getAttribute("x2"), 0.0);
|
||||
double y2 = SvgStyleUtil.parseLength(e.getAttribute("y2"), 0.0);
|
||||
Path2D.Double out = new Path2D.Double();
|
||||
out.moveTo(x1, y1);
|
||||
out.lineTo(x2, y2);
|
||||
return out;
|
||||
}
|
||||
|
||||
private static @Nullable Path2D.Double buildPoly(Element e, boolean closed) {
|
||||
String points = e.getAttribute("points");
|
||||
if (points == null || points.isBlank()) return null;
|
||||
double[] vals = SvgStyleUtil.parseNumberList(points);
|
||||
if (vals.length < 4) return null;
|
||||
Path2D.Double out = new Path2D.Double();
|
||||
out.moveTo(vals[0], vals[1]);
|
||||
for (int i = 2; i + 1 < vals.length; i += 2) {
|
||||
out.lineTo(vals[i], vals[i + 1]);
|
||||
}
|
||||
if (closed) out.closePath();
|
||||
return out;
|
||||
}
|
||||
|
||||
private static void render(Renderer2D renderer,
|
||||
SvgDoc doc,
|
||||
double x,
|
||||
double y,
|
||||
double width,
|
||||
double height,
|
||||
SvgRenderOptions options) {
|
||||
if (doc.vw <= EPS || doc.vh <= EPS) return;
|
||||
|
||||
if (options.textureCache()) {
|
||||
CompiledTexture texture = getOrRasterize(doc, width, height, options);
|
||||
if (texture != null && texture.isReady()) {
|
||||
traceOnce("svg-texture-draw:" + doc.cacheKey + ':' + texture.widthPx + 'x' + texture.heightPx + ':' + texture.scale,
|
||||
"[LoVisual][SVG] GPU texture path active: doc=%s tex=%dx%d logical=%.2fx%.2f scale=%d",
|
||||
doc.cacheKey, texture.widthPx, texture.heightPx, Math.abs(width), Math.abs(height), texture.scale);
|
||||
renderer.textureQuad(texture.texture.getTextureView(), texture.texture.getSampler(), x, y, width, height, textureTint(options));
|
||||
return;
|
||||
}
|
||||
|
||||
traceOnce("svg-texture-fallback:" + doc.cacheKey + ':' + quantize(width, 16.0) + 'x' + quantize(height, 16.0),
|
||||
"[LoVisual][SVG] GPU texture path unavailable, falling back to mesh: doc=%s logical=%.2fx%.2f",
|
||||
doc.cacheKey, Math.abs(width), Math.abs(height));
|
||||
}
|
||||
|
||||
CompiledGeometry geometry = getOrCompile(doc, width, height, options);
|
||||
renderer.quadBatch(batch -> {
|
||||
for (QuadCmd quad : geometry.quads) {
|
||||
batch.quad(x + quad.x, y + quad.y, quad.w, quad.h, quad.argb);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static @Nullable CompiledTexture getOrRasterize(SvgDoc doc,
|
||||
double width,
|
||||
double height,
|
||||
SvgRenderOptions options) {
|
||||
double logicalW = Math.abs(width);
|
||||
double logicalH = Math.abs(height);
|
||||
if (logicalW <= EPS || logicalH <= EPS) return null;
|
||||
|
||||
PhysicalSize physical = physicalDrawSize(logicalW, logicalH);
|
||||
int scale = rasterScaleFor(physical.width, physical.height, options.rasterScale());
|
||||
int texW = quantizeTextureDimension(Math.max(1, (int) Math.ceil(physical.width * scale)));
|
||||
int texH = quantizeTextureDimension(Math.max(1, (int) Math.ceil(physical.height * scale)));
|
||||
if (texW <= 0 || texH <= 0) return null;
|
||||
|
||||
int gradientAngle1024 = options.colorMode() == SvgColorMode.GRADIENT_LINEAR
|
||||
? quantize(options.gradientAngleDegrees(), 1024.0)
|
||||
: 0;
|
||||
int alpha1024 = quantize(options.alpha(), 1024.0);
|
||||
TextureKey key = new TextureKey(
|
||||
doc.cacheKey,
|
||||
texW,
|
||||
texH,
|
||||
options.colorMode(),
|
||||
scale,
|
||||
options.colorMode() == SvgColorMode.GRADIENT_LINEAR ? options.gradientStartArgb() : 0,
|
||||
options.colorMode() == SvgColorMode.GRADIENT_LINEAR ? options.gradientEndArgb() : 0,
|
||||
gradientAngle1024,
|
||||
options.colorMode() == SvgColorMode.GRADIENT_LINEAR ? alpha1024 : 0
|
||||
);
|
||||
|
||||
synchronized (TEXTURE_CACHE) {
|
||||
CompiledTexture cached = TEXTURE_CACHE.get(key);
|
||||
if (cached != null) {
|
||||
traceOnce("svg-texture-cache-hit:" + key,
|
||||
"[LoVisual][SVG] Texture cache hit: doc=%s tex=%dx%d scale=%d",
|
||||
doc.cacheKey, texW, texH, scale);
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
traceOnce("svg-texture-rasterize:" + key,
|
||||
"[LoVisual][SVG] Rasterizing AA texture: doc=%s tex=%dx%d logical=%.2fx%.2f physical=%.2fx%.2f scale=%d",
|
||||
doc.cacheKey, texW, texH, logicalW, logicalH, physical.width, physical.height, scale);
|
||||
CompiledTexture compiled = rasterizeTexture(doc, texW, texH, options, scale);
|
||||
if (compiled == null) return null;
|
||||
|
||||
synchronized (TEXTURE_CACHE) {
|
||||
CompiledTexture raced = TEXTURE_CACHE.get(key);
|
||||
if (raced != null) {
|
||||
compiled.close();
|
||||
return raced;
|
||||
}
|
||||
TEXTURE_CACHE.put(key, compiled);
|
||||
}
|
||||
return compiled;
|
||||
}
|
||||
|
||||
private static PhysicalSize physicalDrawSize(double logicalW, double logicalH) {
|
||||
ViewportContext viewport = ViewportContext.current();
|
||||
if (viewport == null || viewport.width() <= EPS || viewport.height() <= EPS) {
|
||||
return new PhysicalSize(logicalW, logicalH);
|
||||
}
|
||||
|
||||
double scaleX = viewport.framebufferWidth() / Math.max(EPS, Math.abs(viewport.width()));
|
||||
double scaleY = viewport.framebufferHeight() / Math.max(EPS, Math.abs(viewport.height()));
|
||||
if (!Double.isFinite(scaleX) || scaleX <= EPS) scaleX = 1.0;
|
||||
if (!Double.isFinite(scaleY) || scaleY <= EPS) scaleY = 1.0;
|
||||
return new PhysicalSize(logicalW * scaleX, logicalH * scaleY);
|
||||
}
|
||||
|
||||
private static int rasterScaleFor(double width, double height, int requestedScale) {
|
||||
int scale = Math.max(1, Math.min(4, requestedScale));
|
||||
while (scale > 1) {
|
||||
int texW = quantizeTextureDimension(Math.max(1, (int) Math.ceil(width * scale)));
|
||||
int texH = quantizeTextureDimension(Math.max(1, (int) Math.ceil(height * scale)));
|
||||
long pixels = (long) texW * (long) texH;
|
||||
if (texW <= MAX_TEXTURE_SIDE && texH <= MAX_TEXTURE_SIDE && pixels <= MAX_TEXTURE_PIXELS) {
|
||||
return scale;
|
||||
}
|
||||
scale--;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int quantizeTextureDimension(int pixels) {
|
||||
if (pixels <= 1) return 1;
|
||||
int quantum = pixels <= 128
|
||||
? SMALL_TEXTURE_QUANTUM
|
||||
: pixels <= 512 ? MEDIUM_TEXTURE_QUANTUM : LARGE_TEXTURE_QUANTUM;
|
||||
return ((pixels + quantum - 1) / quantum) * quantum;
|
||||
}
|
||||
|
||||
private static @Nullable CompiledTexture rasterizeTexture(SvgDoc doc,
|
||||
int texW,
|
||||
int texH,
|
||||
SvgRenderOptions options,
|
||||
int scale) {
|
||||
try {
|
||||
BufferedImage image = new BufferedImage(texW, texH, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g = image.createGraphics();
|
||||
try {
|
||||
g.setComposite(AlphaComposite.SrcOver);
|
||||
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
|
||||
g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
|
||||
g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
|
||||
|
||||
AffineTransform view = new AffineTransform();
|
||||
view.scale(texW / doc.vw, texH / doc.vh);
|
||||
view.translate(-doc.vx, -doc.vy);
|
||||
|
||||
SvgRenderOptions rasterOptions = options.withAlpha(1.0f);
|
||||
boolean overrideMask = options.colorMode() == SvgColorMode.OVERRIDE;
|
||||
for (SvgShape shape : doc.shapes) {
|
||||
AffineTransform tx = new AffineTransform(view);
|
||||
tx.concatenate(shape.transform);
|
||||
paintShape(g, shape, tx, rasterOptions, overrideMask, texW, texH);
|
||||
}
|
||||
} finally {
|
||||
g.dispose();
|
||||
}
|
||||
|
||||
Texture texture = uploadTexture(image);
|
||||
if (texture != null) {
|
||||
traceOnce("svg-texture-upload:" + doc.cacheKey + ':' + texW + 'x' + texH + ':' + scale,
|
||||
"[LoVisual][SVG] Uploaded GPU texture: doc=%s tex=%dx%d scale=%d",
|
||||
doc.cacheKey, texW, texH, scale);
|
||||
}
|
||||
return texture == null ? null : new CompiledTexture(texture, texW, texH, scale);
|
||||
} catch (Throwable t) {
|
||||
DebugLog.warn("[LoVisual][SVG] Texture rasterization failed: %s", t.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void paintShape(Graphics2D g,
|
||||
SvgShape shape,
|
||||
AffineTransform tx,
|
||||
SvgRenderOptions options,
|
||||
boolean overrideMask,
|
||||
int viewportW,
|
||||
int viewportH) {
|
||||
Shape transformed = tx.createTransformedShape(shape.path);
|
||||
boolean gradient = options.colorMode() == SvgColorMode.GRADIENT_LINEAR;
|
||||
int fill = overrideMask || gradient
|
||||
? resolveMaskPaint(shape.style.fill, shape.style.fillOpacity, shape.style.groupOpacity, options.alpha())
|
||||
: SvgStyleUtil.resolvePaint(shape.style.fill, shape.style.fillOpacity, shape.style.groupOpacity, options);
|
||||
if (((fill >>> 24) & 0xFF) > 0) {
|
||||
if (gradient) g.setPaint(linearGradientPaint(options, viewportW, viewportH, fill));
|
||||
else g.setColor(new Color(fill, true));
|
||||
g.fill(transformed);
|
||||
}
|
||||
|
||||
int stroke = overrideMask || gradient
|
||||
? resolveMaskPaint(shape.style.stroke, shape.style.strokeOpacity, shape.style.groupOpacity, options.alpha())
|
||||
: SvgStyleUtil.resolvePaint(shape.style.stroke, shape.style.strokeOpacity, shape.style.groupOpacity, options);
|
||||
if (((stroke >>> 24) & 0xFF) <= 0) return;
|
||||
|
||||
double scale = estimateScale(tx);
|
||||
float strokeWidth = Math.max(0.0f, (float) (shape.style.strokeWidth * scale));
|
||||
if (strokeWidth <= 0.0f) return;
|
||||
|
||||
BasicStroke bs = new BasicStroke(strokeWidth, shape.style.lineCap, shape.style.lineJoin, shape.style.miterLimit);
|
||||
Shape stroked = bs.createStrokedShape(transformed);
|
||||
if (gradient) g.setPaint(linearGradientPaint(options, viewportW, viewportH, stroke));
|
||||
else g.setColor(new Color(stroke, true));
|
||||
g.fill(stroked);
|
||||
}
|
||||
|
||||
private static int resolveMaskPaint(Integer paint, float channelOpacity, float groupOpacity, float alpha) {
|
||||
if (paint == null) return 0;
|
||||
int baseA = (paint >>> 24) & 0xFF;
|
||||
int outAlpha = clamp255(Math.round(baseA * clamp01(channelOpacity) * clamp01(groupOpacity) * clamp01(alpha)));
|
||||
return (outAlpha << 24) | 0x00FFFFFF;
|
||||
}
|
||||
|
||||
private static Paint linearGradientPaint(SvgRenderOptions options, int viewportW, int viewportH, int alphaMask) {
|
||||
float w = Math.max(1f, viewportW);
|
||||
float h = Math.max(1f, viewportH);
|
||||
double angle = Math.toRadians(options.gradientAngleDegrees());
|
||||
float dx = (float) Math.cos(angle);
|
||||
float dy = (float) Math.sin(angle);
|
||||
float extent = Math.max(1f, Math.abs(dx) * w + Math.abs(dy) * h);
|
||||
float cx = w * 0.5f;
|
||||
float cy = h * 0.5f;
|
||||
Color start = gradientColor(options.gradientStartArgb(), alphaMask);
|
||||
Color end = gradientColor(options.gradientEndArgb(), alphaMask);
|
||||
return new LinearGradientPaint(
|
||||
cx - dx * extent * 0.5f,
|
||||
cy - dy * extent * 0.5f,
|
||||
cx + dx * extent * 0.5f,
|
||||
cy + dy * extent * 0.5f,
|
||||
new float[]{0f, 1f},
|
||||
new Color[]{start, end}
|
||||
);
|
||||
}
|
||||
|
||||
private static Color gradientColor(int argb, int alphaMask) {
|
||||
int maskA = (alphaMask >>> 24) & 0xFF;
|
||||
int colorA = (argb >>> 24) & 0xFF;
|
||||
int a = clamp255(Math.round(maskA * (colorA / 255.0f)));
|
||||
return new Color((argb >>> 16) & 0xFF, (argb >>> 8) & 0xFF, argb & 0xFF, a);
|
||||
}
|
||||
|
||||
private static int textureTint(SvgRenderOptions options) {
|
||||
if (options.colorMode() == SvgColorMode.OVERRIDE) {
|
||||
int override = options.overrideArgb();
|
||||
int alpha = clamp255(Math.round(((override >>> 24) & 0xFF) * clamp01(options.alpha())));
|
||||
return (alpha << 24) | (override & 0x00FFFFFF);
|
||||
}
|
||||
|
||||
int alpha = clamp255(Math.round(255.0f * clamp01(options.alpha())));
|
||||
return (alpha << 24) | 0x00FFFFFF;
|
||||
}
|
||||
|
||||
private static float clamp01(float value) {
|
||||
if (value <= 0.0f) return 0.0f;
|
||||
if (value >= 1.0f) return 1.0f;
|
||||
return value;
|
||||
}
|
||||
|
||||
private static int clamp255(int value) {
|
||||
if (value <= 0) return 0;
|
||||
if (value >= 255) return 255;
|
||||
return value;
|
||||
}
|
||||
|
||||
private static @Nullable Texture uploadTexture(BufferedImage image) {
|
||||
int w = image.getWidth();
|
||||
int h = image.getHeight();
|
||||
int[] pixels = new int[w * h];
|
||||
image.getRGB(0, 0, w, h, pixels, 0, w);
|
||||
bleedTransparentRgb(pixels, w, h);
|
||||
|
||||
ByteBuffer buffer = MemoryUtil.memAlloc(w * h * 4);
|
||||
Texture texture = null;
|
||||
try {
|
||||
for (int argb : pixels) {
|
||||
buffer.put((byte) ((argb >>> 16) & 0xFF));
|
||||
buffer.put((byte) ((argb >>> 8) & 0xFF));
|
||||
buffer.put((byte) (argb & 0xFF));
|
||||
buffer.put((byte) ((argb >>> 24) & 0xFF));
|
||||
}
|
||||
buffer.flip();
|
||||
|
||||
texture = new Texture(w, h, GpuFormat.RGBA8_UNORM, FilterMode.LINEAR, FilterMode.LINEAR, AddressMode.CLAMP_TO_EDGE);
|
||||
texture.upload(buffer);
|
||||
return texture;
|
||||
} catch (Throwable t) {
|
||||
if (texture != null) {
|
||||
try {
|
||||
texture.close();
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
DebugLog.warn("[LoVisual][SVG] Texture upload failed: %s", t.getMessage());
|
||||
return null;
|
||||
} finally {
|
||||
MemoryUtil.memFree(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
private static void bleedTransparentRgb(int[] pixels, int width, int height) {
|
||||
if (pixels == null || width <= 0 || height <= 0) return;
|
||||
|
||||
for (int pass = 0; pass < 3; pass++) {
|
||||
int[] source = pixels.clone();
|
||||
boolean changed = false;
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
int index = y * width + x;
|
||||
if (((source[index] >>> 24) & 0xFF) != 0) continue;
|
||||
|
||||
int r = 0;
|
||||
int g = 0;
|
||||
int b = 0;
|
||||
int count = 0;
|
||||
|
||||
for (int oy = -1; oy <= 1; oy++) {
|
||||
int ny = y + oy;
|
||||
if (ny < 0 || ny >= height) continue;
|
||||
|
||||
for (int ox = -1; ox <= 1; ox++) {
|
||||
if (ox == 0 && oy == 0) continue;
|
||||
int nx = x + ox;
|
||||
if (nx < 0 || nx >= width) continue;
|
||||
|
||||
int neighbor = source[ny * width + nx];
|
||||
int a = (neighbor >>> 24) & 0xFF;
|
||||
if (a == 0) continue;
|
||||
|
||||
r += (neighbor >>> 16) & 0xFF;
|
||||
g += (neighbor >>> 8) & 0xFF;
|
||||
b += neighbor & 0xFF;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (count > 0) {
|
||||
pixels[index] = ((r / count) << 16) | ((g / count) << 8) | (b / count);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!changed) return;
|
||||
}
|
||||
}
|
||||
|
||||
private static CompiledGeometry getOrCompile(SvgDoc doc,
|
||||
double width,
|
||||
double height,
|
||||
SvgRenderOptions options) {
|
||||
int w16 = quantize(width, 16.0);
|
||||
int h16 = quantize(height, 16.0);
|
||||
int alpha1024 = quantize(options.alpha(), 1024.0);
|
||||
int flatness1024 = quantize(options.curveFlatness(), 1024.0);
|
||||
int gradientAngle1024 = quantize(options.gradientAngleDegrees(), 1024.0);
|
||||
GeometryKey key = new GeometryKey(
|
||||
doc.cacheKey,
|
||||
w16,
|
||||
h16,
|
||||
options.colorMode(),
|
||||
options.overrideArgb(),
|
||||
options.gradientStartArgb(),
|
||||
options.gradientEndArgb(),
|
||||
gradientAngle1024,
|
||||
alpha1024,
|
||||
flatness1024
|
||||
);
|
||||
|
||||
synchronized (GEOMETRY_CACHE) {
|
||||
CompiledGeometry cached = GEOMETRY_CACHE.get(key);
|
||||
if (cached != null) {
|
||||
cached.hits++;
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
CompiledGeometry compiled = compileGeometry(doc, width, height, options);
|
||||
synchronized (GEOMETRY_CACHE) {
|
||||
CompiledGeometry raced = GEOMETRY_CACHE.get(key);
|
||||
if (raced != null) {
|
||||
raced.hits++;
|
||||
return raced;
|
||||
}
|
||||
compiled.hits++;
|
||||
GEOMETRY_CACHE.put(key, compiled);
|
||||
}
|
||||
return compiled;
|
||||
}
|
||||
|
||||
private static int quantize(double value, double scale) {
|
||||
static int quantize(double value, double scale) {
|
||||
return (int) Math.round(value * scale);
|
||||
}
|
||||
|
||||
private static CompiledGeometry compileGeometry(SvgDoc doc,
|
||||
double width,
|
||||
double height,
|
||||
SvgRenderOptions options) {
|
||||
ArrayList<QuadCmd> quads = new ArrayList<>(512);
|
||||
QuadSink sink = (x, y, w, h, argb) -> quads.add(new QuadCmd(x, y, w, h, argb));
|
||||
|
||||
AffineTransform view = new AffineTransform();
|
||||
view.scale(width / doc.vw, height / doc.vh);
|
||||
view.translate(-doc.vx, -doc.vy);
|
||||
|
||||
for (SvgShape shape : doc.shapes) {
|
||||
AffineTransform tx = new AffineTransform(view);
|
||||
tx.concatenate(shape.transform);
|
||||
drawShape(shape, tx, width, height, options, sink);
|
||||
}
|
||||
|
||||
return new CompiledGeometry(quads);
|
||||
}
|
||||
|
||||
private static void drawShape(SvgShape shape, AffineTransform tx, double viewportW, double viewportH, SvgRenderOptions options, QuadSink sink) {
|
||||
Shape transformed = tx.createTransformedShape(shape.path);
|
||||
int fill = SvgStyleUtil.resolvePaint(shape.style.fill, shape.style.fillOpacity, shape.style.groupOpacity, options);
|
||||
if (((fill >>> 24) & 0xFF) > 0) {
|
||||
if (options.colorMode() == SvgColorMode.GRADIENT_LINEAR) {
|
||||
fillShapeGradient(transformed, shape.windingRule, fill, options.curveFlatness(), viewportW, viewportH, options, sink);
|
||||
} else {
|
||||
fillShape(transformed, shape.windingRule, fill, options.curveFlatness(), sink);
|
||||
}
|
||||
}
|
||||
|
||||
int stroke = SvgStyleUtil.resolvePaint(shape.style.stroke, shape.style.strokeOpacity, shape.style.groupOpacity, options);
|
||||
if (((stroke >>> 24) & 0xFF) <= 0) return;
|
||||
|
||||
double scale = estimateScale(tx);
|
||||
float strokeWidth = Math.max(0.0f, (float) (shape.style.strokeWidth * scale));
|
||||
if (strokeWidth <= 0.0f) return;
|
||||
|
||||
BasicStroke bs = new BasicStroke(strokeWidth, shape.style.lineCap, shape.style.lineJoin, shape.style.miterLimit);
|
||||
Shape stroked = bs.createStrokedShape(transformed);
|
||||
if (options.colorMode() == SvgColorMode.GRADIENT_LINEAR) {
|
||||
fillShapeGradient(stroked, Path2D.WIND_NON_ZERO, stroke, options.curveFlatness(), viewportW, viewportH, options, sink);
|
||||
} else {
|
||||
fillShape(stroked, Path2D.WIND_NON_ZERO, stroke, options.curveFlatness(), sink);
|
||||
}
|
||||
}
|
||||
|
||||
private static double estimateScale(AffineTransform tx) {
|
||||
double sx = Math.hypot(tx.getScaleX(), tx.getShearX());
|
||||
double sy = Math.hypot(tx.getScaleY(), tx.getShearY());
|
||||
if (sx <= EPS && sy <= EPS) return 1.0;
|
||||
if (sx <= EPS) return sy;
|
||||
if (sy <= EPS) return sx;
|
||||
return 0.5 * (sx + sy);
|
||||
}
|
||||
|
||||
private static void fillShape(Shape shape, int winding, int argb, float flatness, QuadSink sink) {
|
||||
fillShapeInternal(shape, winding, argb, flatness, 1.0, 1.0, null, sink);
|
||||
}
|
||||
|
||||
private static void fillShapeGradient(Shape shape, int winding, int alphaMask, float flatness, double viewportW, double viewportH, SvgRenderOptions options, QuadSink sink) {
|
||||
fillShapeInternal(shape, winding, alphaMask, flatness, viewportW, viewportH, options, sink);
|
||||
}
|
||||
|
||||
private static void fillShapeInternal(Shape shape, int winding, int argb, float flatness, double viewportW, double viewportH, SvgRenderOptions gradientOptions, QuadSink sink) {
|
||||
ArrayList<Edge> edges = new ArrayList<>(128);
|
||||
Bounds b = collectEdges(shape.getPathIterator(null, Math.max(0.05, flatness)), edges);
|
||||
if (edges.isEmpty()) return;
|
||||
|
||||
int yMin = (int) Math.floor(b.minY);
|
||||
int yMax = (int) Math.ceil(b.maxY);
|
||||
if (yMax <= yMin) return;
|
||||
|
||||
ArrayList<Crossing> xs = new ArrayList<>(64);
|
||||
boolean evenOdd = winding == Path2D.WIND_EVEN_ODD;
|
||||
|
||||
for (int y = yMin; y < yMax; y++) {
|
||||
double scanY = y + 0.5;
|
||||
xs.clear();
|
||||
for (Edge e : edges) {
|
||||
if (Math.abs(e.y2 - e.y1) <= EPS) continue;
|
||||
boolean up = e.y1 <= scanY && e.y2 > scanY;
|
||||
boolean down = e.y2 <= scanY && e.y1 > scanY;
|
||||
if (!up && !down) continue;
|
||||
double x = e.x1 + (scanY - e.y1) * (e.x2 - e.x1) / (e.y2 - e.y1);
|
||||
xs.add(new Crossing(x, e.wind));
|
||||
}
|
||||
if (xs.isEmpty()) continue;
|
||||
xs.sort((a, c) -> Double.compare(a.x, c.x));
|
||||
|
||||
if (evenOdd) {
|
||||
for (int i = 0; i + 1 < xs.size(); i += 2) {
|
||||
double x0 = xs.get(i).x;
|
||||
double x1 = xs.get(i + 1).x;
|
||||
if (x1 - x0 > EPS) sink.quad(x0, y, x1 - x0, 1.0, gradientOptions == null ? argb : gradientColor(gradientOptions, argb, x0 + (x1 - x0) * 0.5, scanY, viewportW, viewportH));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
int wind = 0;
|
||||
double start = 0.0;
|
||||
for (Crossing c : xs) {
|
||||
int prev = wind;
|
||||
wind += c.wind;
|
||||
if (prev == 0 && wind != 0) start = c.x;
|
||||
else if (prev != 0 && wind == 0) {
|
||||
if (c.x - start > EPS) sink.quad(start, y, c.x - start, 1.0, gradientOptions == null ? argb : gradientColor(gradientOptions, argb, start + (c.x - start) * 0.5, scanY, viewportW, viewportH));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int gradientColor(SvgRenderOptions options, int alphaMask, double x, double y, double viewportW, double viewportH) {
|
||||
double angle = Math.toRadians(options.gradientAngleDegrees());
|
||||
double dx = Math.cos(angle);
|
||||
double dy = Math.sin(angle);
|
||||
double w = Math.max(1.0, Math.abs(viewportW));
|
||||
double h = Math.max(1.0, Math.abs(viewportH));
|
||||
double extent = Math.abs(dx) * w + Math.abs(dy) * h;
|
||||
double centered = (x - w * 0.5) * dx + (y - h * 0.5) * dy;
|
||||
float t = clamp01((float) (centered / Math.max(1.0, extent) + 0.5));
|
||||
int mixed = mixArgb(options.gradientStartArgb(), options.gradientEndArgb(), t);
|
||||
int maskA = (alphaMask >>> 24) & 0xFF;
|
||||
int mixedA = (mixed >>> 24) & 0xFF;
|
||||
int a = clamp255(Math.round(maskA * (mixedA / 255.0f)));
|
||||
return (a << 24) | (mixed & 0x00FFFFFF);
|
||||
}
|
||||
|
||||
private static int mixArgb(int a, int b, float t) {
|
||||
static int mixArgb(int a, int b, float t) {
|
||||
float k = clamp01(t);
|
||||
int aa = (a >>> 24) & 0xFF;
|
||||
int ar = (a >>> 16) & 0xFF;
|
||||
int ag = (a >>> 8) & 0xFF;
|
||||
int ab = a & 0xFF;
|
||||
int ba = (b >>> 24) & 0xFF;
|
||||
int br = (b >>> 16) & 0xFF;
|
||||
int bg = (b >>> 8) & 0xFF;
|
||||
int bb = b & 0xFF;
|
||||
int oa = Math.round(aa + (ba - aa) * k);
|
||||
int or = Math.round(ar + (br - ar) * k);
|
||||
int og = Math.round(ag + (bg - ag) * k);
|
||||
int ob = Math.round(ab + (bb - ab) * k);
|
||||
int aa = (a >>> 24) & 0xFF; int ar = (a >>> 16) & 0xFF; int ag = (a >>> 8) & 0xFF; int ab = a & 0xFF;
|
||||
int ba = (b >>> 24) & 0xFF; int br = (b >>> 16) & 0xFF; int bg = (b >>> 8) & 0xFF; int bb = b & 0xFF;
|
||||
int oa = Math.round(aa + (ba - aa) * k); int or = Math.round(ar + (br - ar) * k);
|
||||
int og = Math.round(ag + (bg - ag) * k); int ob = Math.round(ab + (bb - ab) * k);
|
||||
return (clamp255(oa) << 24) | (clamp255(or) << 16) | (clamp255(og) << 8) | clamp255(ob);
|
||||
}
|
||||
|
||||
private static Bounds collectEdges(PathIterator it, List<Edge> out) {
|
||||
double[] seg = new double[6];
|
||||
double sx = 0.0;
|
||||
double sy = 0.0;
|
||||
double px = 0.0;
|
||||
double py = 0.0;
|
||||
double minY = Double.POSITIVE_INFINITY;
|
||||
double maxY = Double.NEGATIVE_INFINITY;
|
||||
static float clamp01(float v) { if (v <= 0.0f) return 0.0f; if (v >= 1.0f) return 1.0f; return v; }
|
||||
static int clamp255(int v) { if (v <= 0) return 0; if (v >= 255) return 255; return v; }
|
||||
|
||||
while (!it.isDone()) {
|
||||
int type = it.currentSegment(seg);
|
||||
switch (type) {
|
||||
case PathIterator.SEG_MOVETO -> {
|
||||
sx = seg[0];
|
||||
sy = seg[1];
|
||||
px = sx;
|
||||
py = sy;
|
||||
minY = Math.min(minY, sy);
|
||||
maxY = Math.max(maxY, sy);
|
||||
}
|
||||
case PathIterator.SEG_LINETO -> {
|
||||
addEdge(out, px, py, seg[0], seg[1]);
|
||||
px = seg[0];
|
||||
py = seg[1];
|
||||
minY = Math.min(minY, py);
|
||||
maxY = Math.max(maxY, py);
|
||||
}
|
||||
case PathIterator.SEG_CLOSE -> {
|
||||
addEdge(out, px, py, sx, sy);
|
||||
px = sx;
|
||||
py = sy;
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
it.next();
|
||||
}
|
||||
|
||||
if (!Double.isFinite(minY) || !Double.isFinite(maxY)) return new Bounds(0.0, 0.0);
|
||||
return new Bounds(minY, maxY);
|
||||
}
|
||||
|
||||
private static void addEdge(List<Edge> out, double x1, double y1, double x2, double y2) {
|
||||
if (Math.abs(x1 - x2) <= EPS && Math.abs(y1 - y2) <= EPS) return;
|
||||
out.add(new Edge(x1, y1, x2, y2, y1 < y2 ? 1 : -1));
|
||||
}
|
||||
|
||||
private static void traceOnce(String key, String message, Object... args) {
|
||||
static void traceOnce(String key, String message, Object... args) {
|
||||
if (!DebugLog.isEnabled()) return;
|
||||
|
||||
synchronized (TRACE_ONCE_KEYS) {
|
||||
|
|
@ -942,29 +189,29 @@ public enum SvgMeshBackend {
|
|||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface QuadSink {
|
||||
interface QuadSink {
|
||||
public void quad(double x, double y, double w, double h, int argb);
|
||||
}
|
||||
|
||||
private record SvgDoc(String cacheKey, double vx, double vy, double vw, double vh, List<SvgShape> shapes) {
|
||||
record SvgDoc(String cacheKey, double vx, double vy, double vw, double vh, List<SvgShape> shapes) {
|
||||
}
|
||||
|
||||
private record SvgShape(Path2D.Double path, AffineTransform transform, SvgStyle style, int windingRule) {
|
||||
record SvgShape(Path2D.Double path, AffineTransform transform, SvgStyle style, int windingRule) {
|
||||
}
|
||||
|
||||
private record Edge(double x1, double y1, double x2, double y2, int wind) {
|
||||
record Edge(double x1, double y1, double x2, double y2, int wind) {
|
||||
}
|
||||
|
||||
private record Crossing(double x, int wind) {
|
||||
record Crossing(double x, int wind) {
|
||||
}
|
||||
|
||||
private record Bounds(double minY, double maxY) {
|
||||
record Bounds(double minY, double maxY) {
|
||||
}
|
||||
|
||||
private record PhysicalSize(double width, double height) {
|
||||
record PhysicalSize(double width, double height) {
|
||||
}
|
||||
|
||||
private record TextureKey(String cacheKey,
|
||||
record TextureKey(String cacheKey,
|
||||
int widthPx,
|
||||
int heightPx,
|
||||
SvgColorMode colorMode,
|
||||
|
|
@ -975,7 +222,7 @@ public enum SvgMeshBackend {
|
|||
int alphaQ1024) {
|
||||
}
|
||||
|
||||
private record GeometryKey(String cacheKey,
|
||||
record GeometryKey(String cacheKey,
|
||||
int widthQ16,
|
||||
int heightQ16,
|
||||
SvgColorMode colorMode,
|
||||
|
|
@ -987,16 +234,16 @@ public enum SvgMeshBackend {
|
|||
int flatnessQ1024) {
|
||||
}
|
||||
|
||||
private record QuadCmd(double x, double y, double w, double h, int argb) {
|
||||
record QuadCmd(double x, double y, double w, double h, int argb) {
|
||||
}
|
||||
|
||||
private record CompiledTexture(Texture texture, int widthPx, int heightPx, int scale) {
|
||||
record CompiledTexture(Texture texture, int widthPx, int heightPx, int scale) {
|
||||
|
||||
private boolean isReady() {
|
||||
boolean isReady() {
|
||||
return texture != null && texture.isReady();
|
||||
}
|
||||
|
||||
private void close() {
|
||||
void close() {
|
||||
if (texture != null) {
|
||||
try {
|
||||
texture.close();
|
||||
|
|
@ -1006,15 +253,15 @@ public enum SvgMeshBackend {
|
|||
}
|
||||
}
|
||||
|
||||
private static final class CompiledGeometry {
|
||||
private final List<QuadCmd> quads;
|
||||
private int hits;
|
||||
static final class CompiledGeometry {
|
||||
final List<QuadCmd> quads;
|
||||
int hits;
|
||||
|
||||
private CompiledGeometry(List<QuadCmd> quads) {
|
||||
CompiledGeometry(List<QuadCmd> quads) {
|
||||
this.quads = quads;
|
||||
}
|
||||
|
||||
private void close() {
|
||||
void close() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,173 @@
|
|||
package dev.loki.lovisual.render.engine.svg.backend;
|
||||
|
||||
import java.awt.BasicStroke;
|
||||
import java.awt.Shape;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.geom.Path2D;
|
||||
import java.awt.geom.PathIterator;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import dev.loki.lovisual.render.engine.svg.model.SvgColorMode;
|
||||
import dev.loki.lovisual.render.engine.svg.model.SvgRenderOptions;
|
||||
import dev.loki.lovisual.render.engine.svg.model.SvgStyleUtil;
|
||||
|
||||
/**
|
||||
* Геометрия SVG → quads (триангуляция). Вынесено из гиганта SvgMeshBackend.
|
||||
*/
|
||||
final class SvgGeometry {
|
||||
private SvgGeometry() {}
|
||||
|
||||
static SvgMeshBackend.CompiledGeometry getOrCompile(SvgMeshBackend.SvgDoc doc, double width, double height, SvgRenderOptions options) {
|
||||
int w16 = quantize(width, 16.0);
|
||||
int h16 = quantize(height, 16.0);
|
||||
int alpha1024 = quantize(options.alpha(), 1024.0);
|
||||
int flatness1024 = quantize(options.curveFlatness(), 1024.0);
|
||||
int gradientAngle1024 = quantize(options.gradientAngleDegrees(), 1024.0);
|
||||
SvgMeshBackend.GeometryKey key = new SvgMeshBackend.GeometryKey(doc.cacheKey(), w16, h16, options.colorMode(), options.overrideArgb(), options.gradientStartArgb(), options.gradientEndArgb(), gradientAngle1024, alpha1024, flatness1024);
|
||||
synchronized (SvgMeshBackend.GEOMETRY_CACHE) {
|
||||
SvgMeshBackend.CompiledGeometry cached = SvgMeshBackend.GEOMETRY_CACHE.get(key);
|
||||
if (cached != null) { cached.hits++; return cached; }
|
||||
}
|
||||
SvgMeshBackend.CompiledGeometry compiled = compileGeometry(doc, width, height, options);
|
||||
synchronized (SvgMeshBackend.GEOMETRY_CACHE) {
|
||||
SvgMeshBackend.CompiledGeometry raced = SvgMeshBackend.GEOMETRY_CACHE.get(key);
|
||||
if (raced != null) { raced.hits++; return raced; }
|
||||
compiled.hits++; SvgMeshBackend.GEOMETRY_CACHE.put(key, compiled);
|
||||
}
|
||||
return compiled;
|
||||
}
|
||||
|
||||
static int quantize(double value, double scale) { return (int) Math.round(value * scale); }
|
||||
|
||||
static SvgMeshBackend.CompiledGeometry compileGeometry(SvgMeshBackend.SvgDoc doc, double width, double height, SvgRenderOptions options) {
|
||||
ArrayList<SvgMeshBackend.QuadCmd> quads = new ArrayList<>(512);
|
||||
SvgMeshBackend.QuadSink sink = (x, y, w, h, argb) -> quads.add(new SvgMeshBackend.QuadCmd(x, y, w, h, argb));
|
||||
AffineTransform view = new AffineTransform();
|
||||
view.scale(width / doc.vw(), height / doc.vh());
|
||||
view.translate(-doc.vx(), -doc.vy());
|
||||
for (SvgMeshBackend.SvgShape shape : doc.shapes()) {
|
||||
AffineTransform tx = new AffineTransform(view);
|
||||
tx.concatenate(shape.transform());
|
||||
drawShape(shape, tx, width, height, options, sink);
|
||||
}
|
||||
return new SvgMeshBackend.CompiledGeometry(quads);
|
||||
}
|
||||
|
||||
static void drawShape(SvgMeshBackend.SvgShape shape, AffineTransform tx, double viewportW, double viewportH, SvgRenderOptions options, SvgMeshBackend.QuadSink sink) {
|
||||
Shape transformed = tx.createTransformedShape(shape.path());
|
||||
int fill = SvgStyleUtil.resolvePaint(shape.style().fill, shape.style().fillOpacity, shape.style().groupOpacity, options);
|
||||
if (((fill >>> 24) & 0xFF) > 0) {
|
||||
if (options.colorMode() == SvgColorMode.GRADIENT_LINEAR) fillShapeGradient(transformed, shape.windingRule(), fill, options.curveFlatness(), viewportW, viewportH, options, sink);
|
||||
else fillShape(transformed, shape.windingRule(), fill, options.curveFlatness(), sink);
|
||||
}
|
||||
int stroke = SvgStyleUtil.resolvePaint(shape.style().stroke, shape.style().strokeOpacity, shape.style().groupOpacity, options);
|
||||
if (((stroke >>> 24) & 0xFF) <= 0) return;
|
||||
double scale = estimateScale(tx);
|
||||
float strokeWidth = Math.max(0.0f, (float) (shape.style().strokeWidth * scale));
|
||||
if (strokeWidth <= 0.0f) return;
|
||||
BasicStroke bs = new BasicStroke(strokeWidth, shape.style().lineCap, shape.style().lineJoin, shape.style().miterLimit);
|
||||
Shape stroked = bs.createStrokedShape(transformed);
|
||||
if (options.colorMode() == SvgColorMode.GRADIENT_LINEAR) fillShapeGradient(stroked, Path2D.WIND_NON_ZERO, stroke, options.curveFlatness(), viewportW, viewportH, options, sink);
|
||||
else fillShape(stroked, Path2D.WIND_NON_ZERO, stroke, options.curveFlatness(), sink);
|
||||
}
|
||||
|
||||
static double estimateScale(AffineTransform tx) {
|
||||
double sx = Math.hypot(tx.getScaleX(), tx.getShearX());
|
||||
double sy = Math.hypot(tx.getScaleY(), tx.getShearY());
|
||||
if (sx <= SvgMeshBackend.EPS && sy <= SvgMeshBackend.EPS) return 1.0;
|
||||
if (sx <= SvgMeshBackend.EPS) return sy;
|
||||
if (sy <= SvgMeshBackend.EPS) return sx;
|
||||
return 0.5 * (sx + sy);
|
||||
}
|
||||
|
||||
static void fillShape(Shape shape, int winding, int argb, float flatness, SvgMeshBackend.QuadSink sink) { fillShapeInternal(shape, winding, argb, flatness, 1.0, 1.0, null, sink); }
|
||||
|
||||
static void fillShapeGradient(Shape shape, int winding, int alphaMask, float flatness, double viewportW, double viewportH, SvgRenderOptions options, SvgMeshBackend.QuadSink sink) { fillShapeInternal(shape, winding, alphaMask, flatness, viewportW, viewportH, options, sink); }
|
||||
|
||||
static void fillShapeInternal(Shape shape, int winding, int argb, float flatness, double viewportW, double viewportH, SvgRenderOptions gradientOptions, SvgMeshBackend.QuadSink sink) {
|
||||
ArrayList<SvgMeshBackend.Edge> edges = new ArrayList<>(128);
|
||||
SvgMeshBackend.Bounds b = collectEdges(shape.getPathIterator(null, Math.max(0.05, flatness)), edges);
|
||||
if (edges.isEmpty()) return;
|
||||
int yMin = (int) Math.floor(b.minY());
|
||||
int yMax = (int) Math.ceil(b.maxY());
|
||||
if (yMax <= yMin) return;
|
||||
ArrayList<SvgMeshBackend.Crossing> xs = new ArrayList<>(64);
|
||||
boolean evenOdd = winding == Path2D.WIND_EVEN_ODD;
|
||||
for (int y = yMin; y < yMax; y++) {
|
||||
double scanY = y + 0.5;
|
||||
xs.clear();
|
||||
for (SvgMeshBackend.Edge e : edges) {
|
||||
if (Math.abs(e.y2() - e.y1()) <= SvgMeshBackend.EPS) continue;
|
||||
boolean up = e.y1() <= scanY && e.y2() > scanY;
|
||||
boolean down = e.y2() <= scanY && e.y1() > scanY;
|
||||
if (!up && !down) continue;
|
||||
double x = e.x1() + (scanY - e.y1()) * (e.x2() - e.x1()) / (e.y2() - e.y1());
|
||||
xs.add(new SvgMeshBackend.Crossing(x, e.wind()));
|
||||
}
|
||||
if (xs.isEmpty()) continue;
|
||||
xs.sort((a, c) -> Double.compare(a.x(), c.x()));
|
||||
if (evenOdd) {
|
||||
for (int i = 0; i + 1 < xs.size(); i += 2) {
|
||||
double x0 = xs.get(i).x(); double x1 = xs.get(i + 1).x();
|
||||
if (x1 - x0 > SvgMeshBackend.EPS) sink.quad(x0, y, x1 - x0, 1.0, gradientOptions == null ? argb : gradientColor(gradientOptions, argb, x0 + (x1 - x0) * 0.5, scanY, viewportW, viewportH));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
int wind = 0; double start = 0.0;
|
||||
for (SvgMeshBackend.Crossing c : xs) {
|
||||
int prev = wind; wind += c.wind();
|
||||
if (prev == 0 && wind != 0) start = c.x();
|
||||
else if (prev != 0 && wind == 0) {
|
||||
if (c.x() - start > SvgMeshBackend.EPS) sink.quad(start, y, c.x() - start, 1.0, gradientOptions == null ? argb : gradientColor(gradientOptions, argb, start + (c.x() - start) * 0.5, scanY, viewportW, viewportH));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int gradientColor(SvgRenderOptions options, int alphaMask, double x, double y, double viewportW, double viewportH) {
|
||||
double angle = Math.toRadians(options.gradientAngleDegrees());
|
||||
double dx = Math.cos(angle); double dy = Math.sin(angle);
|
||||
double w = Math.max(1.0, Math.abs(viewportW)); double h = Math.max(1.0, Math.abs(viewportH));
|
||||
double extent = Math.abs(dx) * w + Math.abs(dy) * h;
|
||||
double centered = (x - w * 0.5) * dx + (y - h * 0.5) * dy;
|
||||
float t = clamp01((float) (centered / Math.max(1.0, extent) + 0.5));
|
||||
int mixed = mixArgb(options.gradientStartArgb(), options.gradientEndArgb(), t);
|
||||
int maskA = (alphaMask >>> 24) & 0xFF; int mixedA = (mixed >>> 24) & 0xFF;
|
||||
int a = clamp255(Math.round(maskA * (mixedA / 255.0f)));
|
||||
return (a << 24) | (mixed & 0x00FFFFFF);
|
||||
}
|
||||
|
||||
static int mixArgb(int a, int b, float t) {
|
||||
float k = clamp01(t);
|
||||
int aa = (a >>> 24) & 0xFF; int ar = (a >>> 16) & 0xFF; int ag = (a >>> 8) & 0xFF; int ab = a & 0xFF;
|
||||
int ba = (b >>> 24) & 0xFF; int br = (b >>> 16) & 0xFF; int bg = (b >>> 8) & 0xFF; int bb = b & 0xFF;
|
||||
int oa = Math.round(aa + (ba - aa) * k); int or = Math.round(ar + (br - ar) * k);
|
||||
int og = Math.round(ag + (bg - ag) * k); int ob = Math.round(ab + (bb - ab) * k);
|
||||
return (clamp255(oa) << 24) | (clamp255(or) << 16) | (clamp255(og) << 8) | clamp255(ob);
|
||||
}
|
||||
|
||||
static SvgMeshBackend.Bounds collectEdges(PathIterator it, List<SvgMeshBackend.Edge> out) {
|
||||
double[] seg = new double[6]; double sx = 0.0, sy = 0.0, px = 0.0, py = 0.0;
|
||||
double minY = Double.POSITIVE_INFINITY, maxY = Double.NEGATIVE_INFINITY;
|
||||
while (!it.isDone()) {
|
||||
int type = it.currentSegment(seg);
|
||||
switch (type) {
|
||||
case PathIterator.SEG_MOVETO -> { sx = seg[0]; sy = seg[1]; px = sx; py = sy; minY = Math.min(minY, sy); maxY = Math.max(maxY, sy); }
|
||||
case PathIterator.SEG_LINETO -> { addEdge(out, px, py, seg[0], seg[1]); px = seg[0]; py = seg[1]; minY = Math.min(minY, py); maxY = Math.max(maxY, py); }
|
||||
case PathIterator.SEG_CLOSE -> { addEdge(out, px, py, sx, sy); px = sx; py = sy; }
|
||||
default -> {}
|
||||
}
|
||||
it.next();
|
||||
}
|
||||
if (!Double.isFinite(minY) || !Double.isFinite(maxY)) return new SvgMeshBackend.Bounds(0.0, 0.0);
|
||||
return new SvgMeshBackend.Bounds(minY, maxY);
|
||||
}
|
||||
|
||||
static void addEdge(List<SvgMeshBackend.Edge> out, double x1, double y1, double x2, double y2) {
|
||||
if (Math.abs(x1 - x2) <= SvgMeshBackend.EPS && Math.abs(y1 - y2) <= SvgMeshBackend.EPS) return;
|
||||
out.add(new SvgMeshBackend.Edge(x1, y1, x2, y2, y1 < y2 ? 1 : -1));
|
||||
}
|
||||
|
||||
static float clamp01(float v) { if (v <= 0.0f) return 0.0f; if (v >= 1.0f) return 1.0f; return v; }
|
||||
static int clamp255(int v) { if (v <= 0) return 0; if (v >= 255) return 255; return v; }
|
||||
}
|
||||
|
|
@ -0,0 +1,181 @@
|
|||
package dev.loki.lovisual.render.engine.svg.backend;
|
||||
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import javax.xml.XMLConstants;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.geom.Ellipse2D;
|
||||
import java.awt.geom.Path2D;
|
||||
import java.awt.geom.RoundRectangle2D;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import dev.loki.lovisual.render.engine.svg.model.SvgStyle;
|
||||
import dev.loki.lovisual.render.engine.svg.model.SvgStyleUtil;
|
||||
import dev.loki.lovisual.render.engine.svg.parser.SvgPathParser;
|
||||
import dev.loki.lovisual.util.logging.DebugLog;
|
||||
|
||||
/**
|
||||
* Парсинг SVG → SvgDoc (viewBox + shapes). Вынесено из гиганта SvgMeshBackend.
|
||||
*/
|
||||
final class SvgParser {
|
||||
private SvgParser() {}
|
||||
|
||||
static @Nullable SvgMeshBackend.SvgDoc parseSvg(InputStream in, String cacheKey) {
|
||||
try {
|
||||
DocumentBuilderFactory f = DocumentBuilderFactory.newInstance();
|
||||
f.setNamespaceAware(false);
|
||||
f.setXIncludeAware(false);
|
||||
f.setExpandEntityReferences(false);
|
||||
f.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
|
||||
safeSet(f, "http://apache.org/xml/features/disallow-doctype-decl", true);
|
||||
safeSet(f, "http://xml.org/sax/features/external-general-entities", false);
|
||||
safeSet(f, "http://xml.org/sax/features/external-parameter-entities", false);
|
||||
safeSet(f, "http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
|
||||
DocumentBuilder b = f.newDocumentBuilder();
|
||||
Document doc = b.parse(in);
|
||||
Element root = doc.getDocumentElement();
|
||||
if (root == null) return null;
|
||||
double[] vb = parseViewBox(root);
|
||||
SvgStyle rootStyle = SvgStyle.root();
|
||||
ArrayList<SvgMeshBackend.SvgShape> shapes = new ArrayList<>();
|
||||
walk(root, new AffineTransform(), rootStyle, shapes);
|
||||
return new SvgMeshBackend.SvgDoc(cacheKey, vb[0], vb[1], vb[2], vb[3], shapes);
|
||||
} catch (Exception e) {
|
||||
DebugLog.warn("[LoVisual][SVG] Parse failed: %s", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static void safeSet(DocumentBuilderFactory f, String name, boolean value) {
|
||||
try { f.setFeature(name, value); } catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
static double[] parseViewBox(Element root) {
|
||||
String viewBox = root.getAttribute("viewBox");
|
||||
if (viewBox != null && !viewBox.isBlank()) {
|
||||
double[] vals = SvgStyleUtil.parseNumberList(viewBox);
|
||||
if (vals.length == 4 && vals[2] > 0.0 && vals[3] > 0.0) return vals;
|
||||
}
|
||||
double w = SvgStyleUtil.parseLength(root.getAttribute("width"), 24.0);
|
||||
double h = SvgStyleUtil.parseLength(root.getAttribute("height"), 24.0);
|
||||
if (w <= 0.0) w = 24.0;
|
||||
if (h <= 0.0) h = 24.0;
|
||||
return new double[]{0.0, 0.0, w, h};
|
||||
}
|
||||
|
||||
static void walk(Element node, AffineTransform parentTransform, SvgStyle parentStyle, List<SvgMeshBackend.SvgShape> out) {
|
||||
String tag = node.getTagName();
|
||||
if (tag == null || tag.isBlank()) return;
|
||||
tag = tag.toLowerCase(Locale.ROOT);
|
||||
SvgStyle style = parentStyle.copy();
|
||||
SvgStyleUtil.applyElementStyle(style, node);
|
||||
if (!style.visible) return;
|
||||
AffineTransform transform = new AffineTransform(parentTransform);
|
||||
transform.concatenate(SvgStyleUtil.parseTransform(node.getAttribute("transform")));
|
||||
switch (tag) {
|
||||
case "svg", "g", "a", "symbol" -> {
|
||||
NodeList children = node.getChildNodes();
|
||||
for (int i = 0; i < children.getLength(); i++) {
|
||||
Node child = children.item(i);
|
||||
if (child instanceof Element childElement) walk(childElement, transform, style, out);
|
||||
}
|
||||
}
|
||||
case "defs" -> {}
|
||||
default -> {
|
||||
Path2D.Double path = buildPath(node, tag);
|
||||
if (path == null) return;
|
||||
int winding = "evenodd".equalsIgnoreCase(style.fillRule) ? Path2D.WIND_EVEN_ODD : Path2D.WIND_NON_ZERO;
|
||||
path.setWindingRule(winding);
|
||||
out.add(new SvgMeshBackend.SvgShape(path, transform, style.copy(), winding));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static @Nullable Path2D.Double buildPath(Element element, String tag) {
|
||||
return switch (tag) {
|
||||
case "path" -> {
|
||||
String d = element.getAttribute("d");
|
||||
if (d == null || d.isBlank()) yield null;
|
||||
yield SvgPathParser.parse(d);
|
||||
}
|
||||
case "rect" -> buildRect(element);
|
||||
case "circle" -> buildCircle(element);
|
||||
case "ellipse" -> buildEllipse(element);
|
||||
case "line" -> buildLine(element);
|
||||
case "polyline" -> buildPoly(element, false);
|
||||
case "polygon" -> buildPoly(element, true);
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
static @Nullable Path2D.Double buildRect(Element e) {
|
||||
double x = SvgStyleUtil.parseLength(e.getAttribute("x"), 0.0);
|
||||
double y = SvgStyleUtil.parseLength(e.getAttribute("y"), 0.0);
|
||||
double w = SvgStyleUtil.parseLength(e.getAttribute("width"), 0.0);
|
||||
double h = SvgStyleUtil.parseLength(e.getAttribute("height"), 0.0);
|
||||
if (w <= 0.0 || h <= 0.0) return null;
|
||||
double rx = SvgStyleUtil.parseLength(e.getAttribute("rx"), -1.0);
|
||||
double ry = SvgStyleUtil.parseLength(e.getAttribute("ry"), -1.0);
|
||||
if (rx < 0.0 && ry >= 0.0) rx = ry;
|
||||
if (ry < 0.0 && rx >= 0.0) ry = rx;
|
||||
if (rx < 0.0) rx = 0.0;
|
||||
if (ry < 0.0) ry = 0.0;
|
||||
rx = Math.min(rx, w * 0.5);
|
||||
ry = Math.min(ry, h * 0.5);
|
||||
Path2D.Double out = new Path2D.Double();
|
||||
if (rx > 0.0 || ry > 0.0) out.append(new RoundRectangle2D.Double(x, y, w, h, rx * 2.0, ry * 2.0), false);
|
||||
else { out.moveTo(x, y); out.lineTo(x + w, y); out.lineTo(x + w, y + h); out.lineTo(x, y + h); out.closePath(); }
|
||||
return out;
|
||||
}
|
||||
|
||||
static @Nullable Path2D.Double buildCircle(Element e) {
|
||||
double cx = SvgStyleUtil.parseLength(e.getAttribute("cx"), 0.0);
|
||||
double cy = SvgStyleUtil.parseLength(e.getAttribute("cy"), 0.0);
|
||||
double r = SvgStyleUtil.parseLength(e.getAttribute("r"), 0.0);
|
||||
if (r <= 0.0) return null;
|
||||
Path2D.Double out = new Path2D.Double();
|
||||
out.append(new Ellipse2D.Double(cx - r, cy - r, 2.0 * r, 2.0 * r), false);
|
||||
return out;
|
||||
}
|
||||
|
||||
static @Nullable Path2D.Double buildEllipse(Element e) {
|
||||
double cx = SvgStyleUtil.parseLength(e.getAttribute("cx"), 0.0);
|
||||
double cy = SvgStyleUtil.parseLength(e.getAttribute("cy"), 0.0);
|
||||
double rx = SvgStyleUtil.parseLength(e.getAttribute("rx"), 0.0);
|
||||
double ry = SvgStyleUtil.parseLength(e.getAttribute("ry"), 0.0);
|
||||
if (rx <= 0.0 || ry <= 0.0) return null;
|
||||
Path2D.Double out = new Path2D.Double();
|
||||
out.append(new Ellipse2D.Double(cx - rx, cy - ry, 2.0 * rx, 2.0 * ry), false);
|
||||
return out;
|
||||
}
|
||||
|
||||
static Path2D.Double buildLine(Element e) {
|
||||
double x1 = SvgStyleUtil.parseLength(e.getAttribute("x1"), 0.0);
|
||||
double y1 = SvgStyleUtil.parseLength(e.getAttribute("y1"), 0.0);
|
||||
double x2 = SvgStyleUtil.parseLength(e.getAttribute("x2"), 0.0);
|
||||
double y2 = SvgStyleUtil.parseLength(e.getAttribute("y2"), 0.0);
|
||||
Path2D.Double out = new Path2D.Double();
|
||||
out.moveTo(x1, y1); out.lineTo(x2, y2);
|
||||
return out;
|
||||
}
|
||||
|
||||
static @Nullable Path2D.Double buildPoly(Element e, boolean closed) {
|
||||
String points = e.getAttribute("points");
|
||||
if (points == null || points.isBlank()) return null;
|
||||
double[] vals = SvgStyleUtil.parseNumberList(points);
|
||||
if (vals.length < 4) return null;
|
||||
Path2D.Double out = new Path2D.Double();
|
||||
out.moveTo(vals[0], vals[1]);
|
||||
for (int i = 2; i + 1 < vals.length; i += 2) out.lineTo(vals[i], vals[i + 1]);
|
||||
if (closed) out.closePath();
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package dev.loki.lovisual.render.engine.svg.backend;
|
||||
|
||||
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
|
||||
import dev.loki.lovisual.render.engine.svg.model.SvgColorMode;
|
||||
import dev.loki.lovisual.render.engine.svg.model.SvgRenderOptions;
|
||||
|
||||
/**
|
||||
* Рендер SVG → Renderer2D (выбор texture vs mesh).
|
||||
*/
|
||||
final class SvgRender {
|
||||
private SvgRender() {}
|
||||
|
||||
static void render(Renderer2D renderer, SvgMeshBackend.SvgDoc doc, double x, double y, double width, double height, SvgRenderOptions options) {
|
||||
if (doc.vw() <= SvgMeshBackend.EPS || doc.vh() <= SvgMeshBackend.EPS) return;
|
||||
if (options.textureCache()) {
|
||||
SvgMeshBackend.CompiledTexture texture = SvgTexture.getOrRasterize(doc, width, height, options);
|
||||
if (texture != null && texture.isReady()) {
|
||||
SvgMeshBackend.traceOnce("svg-texture-draw:" + doc.cacheKey() + ':' + texture.widthPx() + 'x' + texture.heightPx() + ':' + texture.scale(),
|
||||
"[LoVisual][SVG] GPU texture path active: doc=%s tex=%dx%d logical=%.2fx%.2f scale=%d",
|
||||
doc.cacheKey(), texture.widthPx(), texture.heightPx(), Math.abs(width), Math.abs(height), texture.scale());
|
||||
renderer.textureQuad(texture.texture().getTextureView(), texture.texture().getSampler(), x, y, width, height, SvgTexture.textureTint(options));
|
||||
return;
|
||||
}
|
||||
SvgMeshBackend.traceOnce("svg-texture-fallback:" + doc.cacheKey() + ':' + SvgMeshBackend.quantize(width, 16.0) + 'x' + SvgMeshBackend.quantize(height, 16.0),
|
||||
"[LoVisual][SVG] GPU texture path unavailable, falling back to mesh: doc=%s logical=%.2fx%.2f",
|
||||
doc.cacheKey(), Math.abs(width), Math.abs(height));
|
||||
}
|
||||
SvgMeshBackend.CompiledGeometry geometry = SvgGeometry.getOrCompile(doc, width, height, options);
|
||||
renderer.quadBatch(batch -> {
|
||||
for (SvgMeshBackend.QuadCmd quad : geometry.quads) {
|
||||
batch.quad(x + quad.x(), y + quad.y(), quad.w(), quad.h(), quad.argb());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package dev.loki.lovisual.render.engine.svg.backend;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Кэши SVG — документы, геометрия, текстуры, trace.
|
||||
*/
|
||||
final class SvgCache {
|
||||
private SvgCache() {}
|
||||
static void clearAll() {
|
||||
synchronized (SvgMeshBackend.CACHE) { SvgMeshBackend.CACHE.clear(); }
|
||||
synchronized (SvgMeshBackend.TEXTURE_CACHE) {
|
||||
for (SvgMeshBackend.CompiledTexture t : SvgMeshBackend.TEXTURE_CACHE.values()) t.close();
|
||||
SvgMeshBackend.TEXTURE_CACHE.clear();
|
||||
}
|
||||
synchronized (SvgMeshBackend.GEOMETRY_CACHE) {
|
||||
for (SvgMeshBackend.CompiledGeometry g : SvgMeshBackend.GEOMETRY_CACHE.values()) g.close();
|
||||
SvgMeshBackend.GEOMETRY_CACHE.clear();
|
||||
}
|
||||
synchronized (SvgMeshBackend.TRACE_ONCE_KEYS) { SvgMeshBackend.TRACE_ONCE_KEYS.clear(); }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,223 @@
|
|||
package dev.loki.lovisual.render.engine.svg.backend;
|
||||
|
||||
import com.mojang.blaze3d.GpuFormat;
|
||||
import com.mojang.blaze3d.textures.AddressMode;
|
||||
import com.mojang.blaze3d.textures.FilterMode;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import dev.loki.lovisual.render.engine.texture.Texture;
|
||||
import dev.loki.lovisual.render.engine.core.context.ViewportContext;
|
||||
import dev.loki.lovisual.render.engine.svg.model.SvgColorMode;
|
||||
import dev.loki.lovisual.render.engine.svg.model.SvgRenderOptions;
|
||||
import dev.loki.lovisual.render.engine.svg.model.SvgStyleUtil;
|
||||
import dev.loki.lovisual.util.logging.DebugLog;
|
||||
|
||||
/**
|
||||
* Растеризация SVG → GPU texture. Вынесено из гиганта SvgMeshBackend.
|
||||
*/
|
||||
final class SvgTexture {
|
||||
private SvgTexture() {}
|
||||
|
||||
static @Nullable SvgMeshBackend.CompiledTexture getOrRasterize(SvgMeshBackend.SvgDoc doc, double width, double height, SvgRenderOptions options) {
|
||||
double logicalW = Math.abs(width);
|
||||
double logicalH = Math.abs(height);
|
||||
if (logicalW <= SvgMeshBackend.EPS || logicalH <= SvgMeshBackend.EPS) return null;
|
||||
SvgMeshBackend.PhysicalSize physical = physicalDrawSize(logicalW, logicalH);
|
||||
int scale = rasterScaleFor(physical.width(), physical.height(), options.rasterScale());
|
||||
int texW = quantizeTextureDimension(Math.max(1, (int) Math.ceil(physical.width() * scale)));
|
||||
int texH = quantizeTextureDimension(Math.max(1, (int) Math.ceil(physical.height() * scale)));
|
||||
if (texW <= 0 || texH <= 0) return null;
|
||||
int gradientAngle1024 = options.colorMode() == SvgColorMode.GRADIENT_LINEAR ? quantize(options.gradientAngleDegrees(), 1024.0) : 0;
|
||||
int alpha1024 = quantize(options.alpha(), 1024.0);
|
||||
SvgMeshBackend.TextureKey key = new SvgMeshBackend.TextureKey(doc.cacheKey(), texW, texH, options.colorMode(), scale, options.colorMode() == SvgColorMode.GRADIENT_LINEAR ? options.gradientStartArgb() : 0, options.colorMode() == SvgColorMode.GRADIENT_LINEAR ? options.gradientEndArgb() : 0, gradientAngle1024, options.colorMode() == SvgColorMode.GRADIENT_LINEAR ? alpha1024 : 0);
|
||||
synchronized (SvgMeshBackend.TEXTURE_CACHE) {
|
||||
SvgMeshBackend.CompiledTexture cached = SvgMeshBackend.TEXTURE_CACHE.get(key);
|
||||
if (cached != null) {
|
||||
SvgMeshBackend.traceOnce("svg-texture-cache-hit:" + key, "[LoVisual][SVG] Texture cache hit: doc=%s tex=%dx%d scale=%d", doc.cacheKey(), texW, texH, scale);
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
SvgMeshBackend.traceOnce("svg-texture-rasterize:" + key, "[LoVisual][SVG] Rasterizing AA texture: doc=%s tex=%dx%d logical=%.2fx%.2f physical=%.2fx%.2f scale=%d", doc.cacheKey(), texW, texH, logicalW, logicalH, physical.width(), physical.height(), scale);
|
||||
SvgMeshBackend.CompiledTexture compiled = rasterizeTexture(doc, texW, texH, options, scale);
|
||||
if (compiled == null) return null;
|
||||
synchronized (SvgMeshBackend.TEXTURE_CACHE) {
|
||||
SvgMeshBackend.CompiledTexture raced = SvgMeshBackend.TEXTURE_CACHE.get(key);
|
||||
if (raced != null) { compiled.close(); return raced; }
|
||||
SvgMeshBackend.TEXTURE_CACHE.put(key, compiled);
|
||||
}
|
||||
return compiled;
|
||||
}
|
||||
|
||||
static SvgMeshBackend.PhysicalSize physicalDrawSize(double logicalW, double logicalH) {
|
||||
ViewportContext viewport = ViewportContext.current();
|
||||
if (viewport == null || viewport.width() <= SvgMeshBackend.EPS || viewport.height() <= SvgMeshBackend.EPS) return new SvgMeshBackend.PhysicalSize(logicalW, logicalH);
|
||||
double scaleX = viewport.framebufferWidth() / Math.max(SvgMeshBackend.EPS, Math.abs(viewport.width()));
|
||||
double scaleY = viewport.framebufferHeight() / Math.max(SvgMeshBackend.EPS, Math.abs(viewport.height()));
|
||||
if (!Double.isFinite(scaleX) || scaleX <= SvgMeshBackend.EPS) scaleX = 1.0;
|
||||
if (!Double.isFinite(scaleY) || scaleY <= SvgMeshBackend.EPS) scaleY = 1.0;
|
||||
return new SvgMeshBackend.PhysicalSize(logicalW * scaleX, logicalH * scaleY);
|
||||
}
|
||||
|
||||
static int rasterScaleFor(double width, double height, int requestedScale) {
|
||||
int scale = Math.max(1, Math.min(4, requestedScale));
|
||||
while (scale > 1) {
|
||||
int texW = quantizeTextureDimension(Math.max(1, (int) Math.ceil(width * scale)));
|
||||
int texH = quantizeTextureDimension(Math.max(1, (int) Math.ceil(height * scale)));
|
||||
long pixels = (long) texW * (long) texH;
|
||||
if (texW <= SvgMeshBackend.MAX_TEXTURE_SIDE && texH <= SvgMeshBackend.MAX_TEXTURE_SIDE && pixels <= SvgMeshBackend.MAX_TEXTURE_PIXELS) return scale;
|
||||
scale--;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int quantizeTextureDimension(int pixels) {
|
||||
if (pixels <= 1) return 1;
|
||||
int quantum = pixels <= 128 ? SvgMeshBackend.SMALL_TEXTURE_QUANTUM : pixels <= 512 ? SvgMeshBackend.MEDIUM_TEXTURE_QUANTUM : SvgMeshBackend.LARGE_TEXTURE_QUANTUM;
|
||||
return ((pixels + quantum - 1) / quantum) * quantum;
|
||||
}
|
||||
|
||||
static @Nullable SvgMeshBackend.CompiledTexture rasterizeTexture(SvgMeshBackend.SvgDoc doc, int texW, int texH, SvgRenderOptions options, int scale) {
|
||||
try {
|
||||
BufferedImage image = new BufferedImage(texW, texH, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g = image.createGraphics();
|
||||
try {
|
||||
g.setComposite(AlphaComposite.SrcOver);
|
||||
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
|
||||
g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
|
||||
g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
|
||||
AffineTransform view = new AffineTransform();
|
||||
view.scale(texW / doc.vw(), texH / doc.vh());
|
||||
view.translate(-doc.vx(), -doc.vy());
|
||||
SvgRenderOptions rasterOptions = options.withAlpha(1.0f);
|
||||
boolean overrideMask = options.colorMode() == SvgColorMode.OVERRIDE;
|
||||
for (SvgMeshBackend.SvgShape shape : doc.shapes()) {
|
||||
AffineTransform tx = new AffineTransform(view);
|
||||
tx.concatenate(shape.transform());
|
||||
paintShape(g, shape, tx, rasterOptions, overrideMask, texW, texH);
|
||||
}
|
||||
} finally { g.dispose(); }
|
||||
Texture texture = uploadTexture(image);
|
||||
if (texture != null) SvgMeshBackend.traceOnce("svg-texture-upload:" + doc.cacheKey() + ':' + texW + 'x' + texH + ':' + scale, "[LoVisual][SVG] Uploaded GPU texture: doc=%s tex=%dx%d scale=%d", doc.cacheKey(), texW, texH, scale);
|
||||
return texture == null ? null : new SvgMeshBackend.CompiledTexture(texture, texW, texH, scale);
|
||||
} catch (Throwable t) {
|
||||
DebugLog.warn("[LoVisual][SVG] Texture rasterization failed: %s", t.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static void paintShape(Graphics2D g, SvgMeshBackend.SvgShape shape, AffineTransform tx, SvgRenderOptions options, boolean overrideMask, int viewportW, int viewportH) {
|
||||
Shape transformed = tx.createTransformedShape(shape.path());
|
||||
boolean gradient = options.colorMode() == SvgColorMode.GRADIENT_LINEAR;
|
||||
int fill = overrideMask || gradient ? resolveMaskPaint(shape.style().fill, shape.style().fillOpacity, shape.style().groupOpacity, options.alpha()) : SvgStyleUtil.resolvePaint(shape.style().fill, shape.style().fillOpacity, shape.style().groupOpacity, options);
|
||||
if (((fill >>> 24) & 0xFF) > 0) {
|
||||
if (gradient) g.setPaint(linearGradientPaint(options, viewportW, viewportH, fill));
|
||||
else g.setColor(new Color(fill, true));
|
||||
g.fill(transformed);
|
||||
}
|
||||
int stroke = overrideMask || gradient ? resolveMaskPaint(shape.style().stroke, shape.style().strokeOpacity, shape.style().groupOpacity, options.alpha()) : SvgStyleUtil.resolvePaint(shape.style().stroke, shape.style().strokeOpacity, shape.style().groupOpacity, options);
|
||||
if (((stroke >>> 24) & 0xFF) <= 0) return;
|
||||
double scale = SvgGeometry.estimateScale(tx);
|
||||
float strokeWidth = Math.max(0.0f, (float) (shape.style().strokeWidth * scale));
|
||||
if (strokeWidth <= 0.0f) return;
|
||||
BasicStroke bs = new BasicStroke(strokeWidth, shape.style().lineCap, shape.style().lineJoin, shape.style().miterLimit);
|
||||
Shape stroked = bs.createStrokedShape(transformed);
|
||||
if (gradient) g.setPaint(linearGradientPaint(options, viewportW, viewportH, stroke));
|
||||
else g.setColor(new Color(stroke, true));
|
||||
g.fill(stroked);
|
||||
}
|
||||
|
||||
static int resolveMaskPaint(Integer paint, float channelOpacity, float groupOpacity, float alpha) {
|
||||
if (paint == null) return 0;
|
||||
int baseA = (paint >>> 24) & 0xFF;
|
||||
int outAlpha = SvgMeshBackend.clamp255(Math.round(baseA * SvgMeshBackend.clamp01(channelOpacity) * SvgMeshBackend.clamp01(groupOpacity) * SvgMeshBackend.clamp01(alpha)));
|
||||
return (outAlpha << 24) | 0x00FFFFFF;
|
||||
}
|
||||
|
||||
static Paint linearGradientPaint(SvgRenderOptions options, int viewportW, int viewportH, int alphaMask) {
|
||||
float w = Math.max(1f, viewportW); float h = Math.max(1f, viewportH);
|
||||
double angle = Math.toRadians(options.gradientAngleDegrees());
|
||||
float dx = (float) Math.cos(angle); float dy = (float) Math.sin(angle);
|
||||
float extent = Math.max(1f, Math.abs(dx) * w + Math.abs(dy) * h);
|
||||
float cx = w * 0.5f; float cy = h * 0.5f;
|
||||
Color start = gradientColor(options.gradientStartArgb(), alphaMask);
|
||||
Color end = gradientColor(options.gradientEndArgb(), alphaMask);
|
||||
return new LinearGradientPaint(cx - dx * extent * 0.5f, cy - dy * extent * 0.5f, cx + dx * extent * 0.5f, cy + dy * extent * 0.5f, new float[]{0f, 1f}, new Color[]{start, end});
|
||||
}
|
||||
|
||||
static Color gradientColor(int argb, int alphaMask) {
|
||||
int maskA = (alphaMask >>> 24) & 0xFF; int colorA = (argb >>> 24) & 0xFF;
|
||||
int a = SvgMeshBackend.clamp255(Math.round(maskA * (colorA / 255.0f)));
|
||||
return new Color((argb >>> 16) & 0xFF, (argb >>> 8) & 0xFF, argb & 0xFF, a);
|
||||
}
|
||||
|
||||
static int textureTint(SvgRenderOptions options) {
|
||||
if (options.colorMode() == SvgColorMode.OVERRIDE) {
|
||||
int override = options.overrideArgb();
|
||||
int alpha = SvgMeshBackend.clamp255(Math.round(((override >>> 24) & 0xFF) * SvgMeshBackend.clamp01(options.alpha())));
|
||||
return (alpha << 24) | (override & 0x00FFFFFF);
|
||||
}
|
||||
int alpha = SvgMeshBackend.clamp255(Math.round(255.0f * SvgMeshBackend.clamp01(options.alpha())));
|
||||
return (alpha << 24) | 0x00FFFFFF;
|
||||
}
|
||||
|
||||
static @Nullable Texture uploadTexture(BufferedImage image) {
|
||||
int w = image.getWidth(); int h = image.getHeight();
|
||||
int[] pixels = new int[w * h];
|
||||
image.getRGB(0, 0, w, h, pixels, 0, w);
|
||||
bleedTransparentRgb(pixels, w, h);
|
||||
ByteBuffer buffer = MemoryUtil.memAlloc(w * h * 4);
|
||||
Texture texture = null;
|
||||
try {
|
||||
for (int argb : pixels) {
|
||||
buffer.put((byte) ((argb >>> 16) & 0xFF));
|
||||
buffer.put((byte) ((argb >>> 8) & 0xFF));
|
||||
buffer.put((byte) (argb & 0xFF));
|
||||
buffer.put((byte) ((argb >>> 24) & 0xFF));
|
||||
}
|
||||
buffer.flip();
|
||||
texture = new Texture(w, h, GpuFormat.RGBA8_UNORM, FilterMode.LINEAR, FilterMode.LINEAR, AddressMode.CLAMP_TO_EDGE);
|
||||
texture.upload(buffer);
|
||||
return texture;
|
||||
} catch (Throwable t) {
|
||||
if (texture != null) try { texture.close(); } catch (Throwable ignored) {}
|
||||
DebugLog.warn("[LoVisual][SVG] Texture upload failed: %s", t.getMessage());
|
||||
return null;
|
||||
} finally { MemoryUtil.memFree(buffer); }
|
||||
}
|
||||
|
||||
static void bleedTransparentRgb(int[] pixels, int width, int height) {
|
||||
if (pixels == null || width <= 0 || height <= 0) return;
|
||||
for (int pass = 0; pass < 3; pass++) {
|
||||
int[] source = pixels.clone(); boolean changed = false;
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
int index = y * width + x;
|
||||
if (((source[index] >>> 24) & 0xFF) != 0) continue;
|
||||
int r = 0, g = 0, b = 0, count = 0;
|
||||
for (int oy = -1; oy <= 1; oy++) {
|
||||
int ny = y + oy; if (ny < 0 || ny >= height) continue;
|
||||
for (int ox = -1; ox <= 1; ox++) {
|
||||
if (ox == 0 && oy == 0) continue;
|
||||
int nx = x + ox; if (nx < 0 || nx >= width) continue;
|
||||
int neighbor = source[ny * width + nx];
|
||||
int a = (neighbor >>> 24) & 0xFF; if (a == 0) continue;
|
||||
r += (neighbor >>> 16) & 0xFF; g += (neighbor >>> 8) & 0xFF; b += neighbor & 0xFF; count++;
|
||||
}
|
||||
}
|
||||
if (count > 0) { pixels[index] = ((r / count) << 16) | ((g / count) << 8) | (b / count); changed = true; }
|
||||
}
|
||||
}
|
||||
if (!changed) return;
|
||||
}
|
||||
}
|
||||
|
||||
static int quantize(double v, double s) { return (int) Math.round(v * s); }
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue