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:
loki5512344 2026-09-22 20:33:03 +02:00
parent 8181f11c5a
commit ec10395511
140 changed files with 5442 additions and 6457 deletions

View file

@ -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/`) не заглядывался копипаст

View file

@ -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)]`-аналогов (мёртвый код удаляется),
мёртвые миксины.

View file

@ -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).

View file

@ -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/**")

View 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>

View 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>

View file

@ -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

View file

@ -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);

View file

@ -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");

View file

@ -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());
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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));
}
}

View file

@ -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();
}
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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) {
}
}

View file

@ -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;

View file

@ -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());
}
}

View file

@ -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;

View file

@ -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));
}
}

View file

@ -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;
}
}

View file

@ -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());
}
}

View file

@ -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;

View file

@ -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),

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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();
}

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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);

View file

@ -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())
);
}

View file

@ -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)
);
}

View file

@ -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)
);
}

View file

@ -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;
}
}

View file

@ -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.

View file

@ -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);
}
}
}

View file

@ -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 {

View file

@ -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)

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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)

View file

@ -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)

View file

@ -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;

View file

@ -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)

View file

@ -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;

View file

@ -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;

View file

@ -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)

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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);
}
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}
}

View file

@ -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();
}
}
}
}
}

View file

@ -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;
}
}
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}

View file

@ -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());
}
}

View file

@ -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);
}
}

View file

@ -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() {
}
}
}

View file

@ -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; }
}

View file

@ -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;
}
}

View file

@ -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());
}
});
}
}

View file

@ -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(); }
}
}

View file

@ -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