diff --git a/combatant-client-26.2/DEVELOPMENT.md b/combatant-client-26.2/DEVELOPMENT.md deleted file mode 100644 index 405f1c8..0000000 --- a/combatant-client-26.2/DEVELOPMENT.md +++ /dev/null @@ -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-.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 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("", "")` -в `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/`) не заглядывался копипаст diff --git a/combatant-client-26.2/DEV_GUIDE.md b/combatant-client-26.2/DEV_GUIDE.md index 7df9f02..c7bc280 100644 --- a/combatant-client-26.2/DEV_GUIDE.md +++ b/combatant-client-26.2/DEV_GUIDE.md @@ -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)]`-аналогов (мёртвый код удаляется), - мёртвые миксины. diff --git a/combatant-client-26.2/TODO.md b/combatant-client-26.2/TODO.md index 772af0f..9aac544 100644 --- a/combatant-client-26.2/TODO.md +++ b/combatant-client-26.2/TODO.md @@ -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). diff --git a/combatant-client-26.2/build.gradle b/combatant-client-26.2/build.gradle index 3833aad..3d82636 100644 --- a/combatant-client-26.2/build.gradle +++ b/combatant-client-26.2/build.gradle @@ -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/**") diff --git a/combatant-client-26.2/config/checkstyle/checkstyle.xml b/combatant-client-26.2/config/checkstyle/checkstyle.xml new file mode 100644 index 0000000..613cbd5 --- /dev/null +++ b/combatant-client-26.2/config/checkstyle/checkstyle.xml @@ -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> diff --git a/combatant-client-26.2/config/pmd/ruleset.xml b/combatant-client-26.2/config/pmd/ruleset.xml new file mode 100644 index 0000000..464f2b9 --- /dev/null +++ b/combatant-client-26.2/config/pmd/ruleset.xml @@ -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> diff --git a/combatant-client-26.2/gradle.properties b/combatant-client-26.2/gradle.properties index 575d761..c2e5b57 100644 --- a/combatant-client-26.2/gradle.properties +++ b/combatant-client-26.2/gradle.properties @@ -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 diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/LoVisual.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/LoVisual.java index 077a52d..ce6a561 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/LoVisual.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/LoVisual.java @@ -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); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/common/CommonSettingSchemas.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/common/CommonSettingSchemas.java index e81b47b..6deaef1 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/common/CommonSettingSchemas.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/common/CommonSettingSchemas.java @@ -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"); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/i18n/MainConfig.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/i18n/MainConfig.java index 348f990..03bfa9b 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/i18n/MainConfig.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/config/i18n/MainConfig.java @@ -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()); - } } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/render/ParticleSpawnEvent.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/render/particles/ParticleSpawnEvent.java similarity index 93% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/render/ParticleSpawnEvent.java rename to combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/render/particles/ParticleSpawnEvent.java index c169278..b75996a 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/render/ParticleSpawnEvent.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/render/particles/ParticleSpawnEvent.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/world/IrisCommand.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/world/integration/IrisCommand.java similarity index 97% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/world/IrisCommand.java rename to combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/world/integration/IrisCommand.java index 70cf496..77481ca 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/world/IrisCommand.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/world/integration/IrisCommand.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/world/XaeroWaypointCommand.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/world/integration/XaeroWaypointCommand.java similarity index 99% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/world/XaeroWaypointCommand.java rename to combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/world/integration/XaeroWaypointCommand.java index 8a07539..5bc2093 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/world/XaeroWaypointCommand.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/world/integration/XaeroWaypointCommand.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/messages/BetterChatMessageRenderer.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/messages/BetterChatMessageRenderer.java index 74f1edc..74e0cce 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/messages/BetterChatMessageRenderer.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/messages/BetterChatMessageRenderer.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/text/BetterChatGlyphRenderer.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/text/BetterChatGlyphRenderer.java index b7ebfd2..982a496 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/text/BetterChatGlyphRenderer.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/text/BetterChatGlyphRenderer.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/text/BetterChatLineLayout.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/text/BetterChatLineLayout.java index abd8c45..f507477 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/text/BetterChatLineLayout.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/text/BetterChatLineLayout.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/text/util/BetterChatFontUtil.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/text/util/BetterChatFontUtil.java index 36360ae..8bc632c 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/text/util/BetterChatFontUtil.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/text/util/BetterChatFontUtil.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/ModulesMenuScreen.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/ModulesMenuScreen.java index c4fd209..9dd4e09 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/ModulesMenuScreen.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/modules/ModulesMenuScreen.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/MenuScreen.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/MenuScreen.java index 7d3d7b2..a9bf959 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/MenuScreen.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/MenuScreen.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/SettingsPanelComponent.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/SettingsPanelComponent.java index aecdfe8..b4e4d76 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/SettingsPanelComponent.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/module/SettingsPanelComponent.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/ClickGuiRichTextRenderer.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/ClickGuiRichTextRenderer.java index 1c2807e..16b92bd 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/ClickGuiRichTextRenderer.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/ClickGuiRichTextRenderer.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/ClickGuiHintOverlay.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/tooltip/ClickGuiHintOverlay.java similarity index 97% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/ClickGuiHintOverlay.java rename to combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/tooltip/ClickGuiHintOverlay.java index 4a940a2..ad1b0cc 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/ClickGuiHintOverlay.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/tooltip/ClickGuiHintOverlay.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/SettingTooltip.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/tooltip/SettingTooltip.java similarity index 98% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/SettingTooltip.java rename to combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/tooltip/SettingTooltip.java index e7b808d..298dbc1 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/SettingTooltip.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/tooltip/SettingTooltip.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/Scoreboard.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/Scoreboard.java index 9226f37..35583c8 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/Scoreboard.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/panels/Scoreboard.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/BetterButtons.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/BetterButtons.java index 88dea00..d2bd72d 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/BetterButtons.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/BetterButtons.java @@ -7,70 +7,36 @@ package dev.loki.lovisual.features.gui.hud.nondraggable.impl.buttons; -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.client.renderer.texture.TextureAtlas; -import net.minecraft.client.renderer.texture.TextureAtlasSprite; -import net.minecraft.data.AtlasIds; -import net.minecraft.network.chat.Component; -import net.minecraft.resources.Identifier; -import net.minecraft.util.FormattedCharSequence; -import net.minecraft.util.Util; import dev.loki.lovisual.config.definition.SettingDef; -import dev.loki.lovisual.config.values.primitive.BooleanValue; -import dev.loki.lovisual.config.values.mode.EnumValue; -import dev.loki.lovisual.config.values.primitive.NumberValue; import dev.loki.lovisual.config.values.color.RGBAColorValue; +import dev.loki.lovisual.config.values.mode.EnumValue; +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; import dev.loki.lovisual.features.gui.hud.base.AbstractHudElement; import dev.loki.lovisual.features.gui.hud.registry.HudElementRegister; -import dev.loki.lovisual.features.gui.hud.config.HudRenderUtil; -import dev.loki.lovisual.mixininterface.entity.IRoundedHitbox; -import dev.loki.lovisual.mixins.accessors.widget.ClickableWidgetAccessor; +import dev.loki.lovisual.features.gui.hud.nondraggable.impl.buttons.render.ButtonSliderRenderer; +import dev.loki.lovisual.features.gui.hud.nondraggable.impl.buttons.render.ButtonSprites; +import dev.loki.lovisual.features.gui.hud.nondraggable.impl.buttons.render.ButtonStyleRenderer; +import dev.loki.lovisual.features.gui.hud.nondraggable.impl.buttons.render.ButtonTextRenderer; +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.features.gui.hud.nondraggable.impl.buttons.state.ButtonQueue; import dev.loki.lovisual.mixins.accessors.hud.TextIconButtonWidgetAccessor; -import dev.loki.lovisual.render.engine.color.RenderColor; -import dev.loki.lovisual.render.engine.core.context.ViewportContext; import dev.loki.lovisual.render.engine.renderer.warp.RenderWarpStack; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.text.font.info.FontInfo; -import dev.loki.lovisual.render.engine.text.font.Fonts; -import dev.loki.lovisual.render.engine.text.render.TextRenderer; -import dev.loki.lovisual.render.helpers.clip.ScissorFunction; -import dev.loki.lovisual.util.text.format.LegacyTextUtil; -import dev.loki.lovisual.util.text.format.TextRenderUtil; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.components.CycleButton; +import net.minecraft.client.gui.components.SpriteIconButton; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; -import java.util.*; - -import static dev.loki.lovisual.features.theme.Theme.theme; -import dev.loki.lovisual.features.gui.hud.nondraggable.impl.tooltips.BetterTooltips; +import java.util.List; @HudElementRegister(order = 10) public final class BetterButtons extends AbstractHudElement { public static final BetterButtons INSTANCE = new BetterButtons(); - private static final float HOVER_SPEED_IN = 20.0f; - private static final float HOVER_SPEED_OUT = 16.0f; - private static final float BUTTON_RADIUS = 6.0f; - private static final float BUTTON_SOFTNESS = 1.2f; - 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 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 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 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 final EnumValue<Style> style = new EnumValue<>("style", Style.DEFAULT, Style.values()); private final RGBAColorValue gradientBaseColor = @@ -124,479 +90,89 @@ public final class BetterButtons extends AbstractHudElement { return INSTANCE; } + // --- frame / queue delegation --- + public static void beginFrame(GuiGraphicsExtractor ctx) { - Minecraft mc = Minecraft.getInstance(); - Screen screen = mc != null ? ClientScreen.current() : null; - if (screen != LAST_SCREEN) { - HOVER.clear(); - LAST_TICK.clear(); - MOUSE_X.clear(); - MOUSE_Y.clear(); - LAST_SCREEN = screen; - } - LAST_CTX = ctx; - QUEUE.clear(); - SCISSOR_STACK.clear(); - BetterTooltips.beginTooltipFrame(); + ButtonQueue.beginFrame(ctx); } public static GuiGraphicsExtractor getLastContext() { - return LAST_CTX; + return ButtonQueue.lastContext(); } 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)); + ButtonQueue.pushScissorContext(x1, y1, x2, y2); } public static void popScissorContext() { - if (!SCISSOR_STACK.isEmpty()) { - SCISSOR_STACK.pop(); - } - } - - private static ScissorRect currentScissor() { - return SCISSOR_STACK.peek(); + ButtonQueue.popScissorContext(); } 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(); + ButtonQueue.flush(); } 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); + ButtonQueue.captureTooltip(widget, mouseX, mouseY); } public static boolean hasPending() { - return !QUEUE.isEmpty(); + return ButtonQueue.hasPending(); } public static boolean hasTooltip() { - return BetterTooltips.hasTooltip(); + return ButtonQueue.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 enqueueButton(AbstractWidget widget, Component text, float hover, boolean enabled, boolean focused) { + ButtonQueue.enqueueButton(widget, text, hover, enabled, focused); } - public static void enqueueSlider(AbstractWidget widget, - float value, - boolean dragging, - float hover) { - QUEUE.add(new SliderCall(widget, value, dragging, hover, currentScissor())); + public static void enqueueSlider(AbstractWidget widget, float value, boolean dragging, float hover) { + ButtonQueue.enqueueSlider(widget, value, dragging, hover); } - 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 enqueueTextIconButton(SpriteIconButton widget, boolean iconOnly, float hover, boolean enabled, boolean focused) { + ButtonQueue.enqueueTextIconButton(widget, iconOnly, hover, enabled, focused); } - 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 enqueueCyclingButton(CycleButton<?> widget, Identifier icon, boolean showLabel, float hover, boolean enabled, boolean focused) { + ButtonQueue.enqueueCyclingButton(widget, icon, showLabel, hover, enabled, focused); } + public static void renderTooltip() { + ButtonQueue.renderTooltip(); + } + + // --- hover / parallax --- + 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; + return ButtonHoverState.updateHover(widget, hovered); } 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; + return ButtonHoverState.updateHover(widget, hovered, mouseX, mouseY); } - private static float smoothHover(float hover) { - float h = Math.max(0.0f, Math.min(1.0f, hover)); - return h * h * (3.0f - 2.0f * h); + // --- button rendering --- + + public static void renderButton(GuiGraphicsExtractor ctx, AbstractWidget widget, Component text, float hover, boolean enabled, boolean focused) { + ButtonStyleRenderer.renderButton(ctx, widget, text, hover, enabled, focused); } - private static ButtonParallax computeParallax(AbstractWidget widget, float hover) { - if (widget == null || hover <= 0.001f || !widget.isActive()) { - return ButtonParallax.NONE; - } - - float w = Math.max(1.0f, widget.getWidth()); - float h = Math.max(1.0f, widget.getHeight()); - float mx = MOUSE_X.getOrDefault(widget, widget.getX() + w * 0.5f); - float my = MOUSE_Y.getOrDefault(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 = smoothHover(hover); - if (ease <= 0.001f) { - return ButtonParallax.NONE; - } - - return new ButtonParallax( - -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 void renderButtonUnscaled(GuiGraphicsExtractor ctx, AbstractWidget widget, Component text, float hover, boolean enabled, boolean focused) { + ButtonStyleRenderer.renderButtonUnscaled(ctx, widget, text, hover, enabled, focused); } - private static RenderWarpStack.Scope pushParallaxWarp(ButtonParallax 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 - ); + static void renderButtonInternal(GuiGraphicsExtractor ctx, AbstractWidget widget, Component text, float hover, boolean enabled, boolean focused, float scale, boolean manageProjection, boolean allowParallax) { + ButtonStyleRenderer.renderInternal(ctx, widget, text, hover, enabled, focused, scale, manageProjection, allowParallax); } - private static float parallaxShiftX(AbstractWidget widget, float hover) { - return computeParallax(widget, hover).shiftX; + public static void renderSlider(GuiGraphicsExtractor ctx, AbstractWidget widget, float value, boolean dragging, float hover) { + ButtonSliderRenderer.render(ctx, widget, value, dragging, hover); } - private static float parallaxShiftY(AbstractWidget widget, float hover) { - return computeParallax(widget, hover).shiftY; - } - - public static void renderButton(GuiGraphicsExtractor ctx, - AbstractWidget widget, - Component text, - float hover, - boolean enabled, - boolean focused) { - renderButtonInternal(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) { - renderButtonInternal(ctx, widget, text, hover, enabled, focused, 1.0f, false, true); - } - - private static void renderButtonInternal(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 = INSTANCE; - Style mode = cfg.style.get(); - float radiusBase = BUTTON_RADIUS; - float softness = 0.0f; - float strokeSoft = 0.0f; - if (mode == Style.GRADIENT) { - softness = BUTTON_SOFTNESS; - strokeSoft = BUTTON_SOFTNESS; - } else if (mode == Style.CUSTOM) { - radiusBase = cfg.customRadius.get(); - softness = cfg.customSoftness.get(); - strokeSoft = cfg.customSoftness.get(); - } - - float radius = Math.min(radiusBase, h * 0.5f); - markRounded(widget, radius); - - float alpha = widget.getAlpha(); - float hoverEase = 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 parallax = allowParallax ? computeParallax(widget, hover) : ButtonParallax.NONE; - - if (manageProjection) { - ViewportContext.beginUnscaled(null); - } - boolean batching = Renderer2D.isBatching(); - if (!batching) { - Renderer2D.COLOR.begin(); - } - - try (RenderWarpStack.Scope ignored = pushParallaxWarp(parallax, xb, yb, wb, hb)) { - if (mode == Style.DEFAULT) { - 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); - } - } else if (mode == Style.GRADIENT) { - int base = cfg.gradientBaseColor.getArgb(); - int grad = cfg.gradientColor.getArgb(); - float intensity = clamp01(cfg.gradientIntensity.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.gradientAngle.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); - } - } else { - int base = cfg.customBg.getArgb(); - int hoverBg = cfg.customBgHover.getArgb(); - int disabledBg = cfg.customBgDisabled.getArgb(); - int bg = enabled ? HudRenderUtil.mixColor(base, hoverBg, hoverEase) : disabledBg; - - int strokeBase = enabled - ? HudRenderUtil.mixColor(cfg.customStroke.getArgb(), cfg.customStrokeHover.getArgb(), hoverEase) - : cfg.customStrokeDisabled.getArgb(); - - float strokeThickness = cfg.customStrokeThickness.get() * scale; - - if (cfg.customGradient.get()) { - float t = clamp01(cfg.customGradientIntensity.get()); - int grad = cfg.customGradientColor.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.customGradientAngle.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); - } - } - - } - - if (!batching) { - Renderer2D.COLOR.render(); - } - if (manageProjection) { - ViewportContext.end(null); - } - - if (text != null && !text.getString().isEmpty()) { - try (RenderWarpStack.Scope ignored = pushParallaxWarp(parallax, x, y, w, h)) { - renderCenteredText(text, x, y, w, h, alpha, enabled, hover, !parallax.active); - } - } - } - - public static void renderSlider(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); - markRounded(widget, radius); - - renderButtonInternal(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 = smoothHover(hover); - int track = HudRenderUtil.mixColor(forceOpaque(theme().surface()), 0xFF000000, 0.65f); - int fill = HudRenderUtil.mixColor(forceOpaque(theme().accentSoft()), forceOpaque(theme().accent()), 0.25f + 0.35f * hoverEase); - int stroke = HudRenderUtil.mixColor(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(); - renderCenteredText(text, x, y, w, h, alpha, widget.isActive(), hover); - } - - public static void renderTextIconButton(GuiGraphicsExtractor ctx, - SpriteIconButton widget, - boolean iconOnly, - float hover, - boolean enabled, - boolean focused) { + public static void renderTextIconButton(GuiGraphicsExtractor ctx, SpriteIconButton widget, boolean iconOnly, float hover, boolean enabled, boolean focused) { if (ctx == null || widget == null) return; renderButton(ctx, widget, null, hover, enabled, focused); @@ -612,9 +188,9 @@ public final class BetterButtons extends AbstractHudElement { } boolean hoverState = widget.isActive() && (widget.isHoveredOrFocused() || focused || hover > 0.001f); Identifier spriteId = accessor.lovisual$getTexture().get(widget.isActive(), hoverState); - float sx = parallaxShiftX(widget, hover); - float sy = parallaxShiftY(widget, hover); - drawGuiSprite(spriteId, Math.round(iconX + sx), Math.round(iconY + sy), texW, texH, widget.getAlpha()); + float sx = ButtonParallax.shiftX(widget, hover); + float sy = ButtonParallax.shiftY(widget, hover); + ButtonSprites.drawGuiSprite(spriteId, Math.round(iconX + sx), Math.round(iconY + sy), texW, texH, widget.getAlpha()); if (!iconOnly) { float left = widget.getX() + 2f; @@ -622,28 +198,21 @@ public final class BetterButtons extends AbstractHudElement { float top = widget.getY(); float bottom = widget.getY() + widget.getHeight(); Component text = widget.getMessage(); - ButtonParallax parallax = computeParallax(widget, hover); - try (RenderWarpStack.Scope ignored = pushParallaxWarp(parallax, widget.getX(), widget.getY(), widget.getWidth(), widget.getHeight())) { - renderLeftAlignedText(text, left, top, right, bottom, widget.getAlpha(), enabled, hover, !parallax.active); + ButtonParallax.Data parallax = ButtonParallax.compute(widget, hover); + try (RenderWarpStack.Scope ignored = ButtonParallax.pushWarp(parallax, widget.getX(), widget.getY(), widget.getWidth(), widget.getHeight())) { + ButtonTextRenderer.renderLeftAligned(text, left, top, right, bottom, widget.getAlpha(), enabled, hover, !parallax.active()); } } - } - public static void renderCyclingButton(GuiGraphicsExtractor ctx, - CycleButton<?> widget, - Identifier icon, - boolean showLabel, - float hover, - boolean enabled, - boolean focused) { + public static void renderCyclingButton(GuiGraphicsExtractor ctx, CycleButton<?> widget, Identifier icon, boolean showLabel, float hover, boolean enabled, boolean focused) { if (ctx == null || widget == null) return; renderButton(ctx, widget, null, hover, enabled, focused); if (icon != null) { int size = Math.min(widget.getWidth(), widget.getHeight()) - 6; - float sx = parallaxShiftX(widget, hover); - float sy = parallaxShiftY(widget, hover); + float sx = ButtonParallax.shiftX(widget, hover); + float sy = ButtonParallax.shiftY(widget, hover); int ix = Math.round(widget.getX() + (widget.getWidth() - size) / 2 + sx); int iy = Math.round(widget.getY() + (widget.getHeight() - size) / 2 + sy); ctx.blitSprite(net.minecraft.client.renderer.RenderPipelines.GUI_TEXTURED, icon, ix, iy, size, size, widget.getAlpha()); @@ -651,258 +220,110 @@ public final class BetterButtons extends AbstractHudElement { if (showLabel) { Component text = widget.getMessage(); - ButtonParallax parallax = computeParallax(widget, hover); - try (RenderWarpStack.Scope ignored = pushParallaxWarp(parallax, widget.getX(), widget.getY(), widget.getWidth(), widget.getHeight())) { - renderCenteredText(text, widget.getX(), widget.getY(), widget.getWidth(), widget.getHeight(), - widget.getAlpha(), enabled, hover, !parallax.active); + ButtonParallax.Data parallax = ButtonParallax.compute(widget, hover); + try (RenderWarpStack.Scope ignored = ButtonParallax.pushWarp(parallax, widget.getX(), widget.getY(), widget.getWidth(), widget.getHeight())) { + ButtonTextRenderer.renderCentered(text, widget.getX(), widget.getY(), widget.getWidth(), widget.getHeight(), + widget.getAlpha(), enabled, hover, !parallax.active()); } } } - public static void renderCenteredText(Component text, - float x, float y, float w, float h, - float alpha, boolean enabled, float hover) { - renderCenteredText(text, x, y, w, h, alpha, enabled, hover, true); + public static void renderCenteredText(Component text, float x, float y, float w, float h, float alpha, boolean enabled, float hover) { + ButtonTextRenderer.renderCentered(text, x, y, w, h, alpha, enabled, hover); } - private static void renderCenteredText(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 = resolveTextPaddingX(); - float scale = resolveTextScaleBase() * (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 renderLeftAlignedText(Component text, float x0, float y0, float x1, float y1, float alpha, boolean enabled, float hover) { + ButtonTextRenderer.renderLeftAligned(text, x0, y0, x1, y1, alpha, enabled, hover); } - public static void renderLeftAlignedText(Component text, - float x0, float y0, - float x1, float y1, - float alpha, boolean enabled, float hover) { - renderLeftAlignedText(text, x0, y0, x1, y1, alpha, enabled, hover, true); + // --- getters for render helpers --- + + public EnumValue<Style> getStyle() { + return style; } - private static void renderLeftAlignedText(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 = resolveTextPaddingX(); - float scale = resolveTextScaleBase() * (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(); + public RGBAColorValue getGradientBaseColor() { + return gradientBaseColor; } - private static float resolveTextScaleBase() { - BetterButtons cfg = INSTANCE; - if (cfg != null && cfg.style.get() == Style.CUSTOM) { - return TEXT_SCALE_BASE * cfg.customTextScale.get(); - } - return TEXT_SCALE_BASE; + public RGBAColorValue getGradientColor() { + return gradientColor; } - private static float resolveTextPaddingX() { - BetterButtons cfg = INSTANCE; - if (cfg != null && cfg.style.get() == Style.CUSTOM) { - return cfg.customTextPadX.get(); - } - return TEXT_PADDING_X; + public NumberValue<Float> getGradientAngle() { + return gradientAngle; } - private static int resolveTextColor(boolean enabled, float hover) { - BetterButtons cfg = INSTANCE; - if (cfg != null && cfg.style.get() == Style.CUSTOM) { - return enabled ? cfg.customText.getArgb() : cfg.customTextDisabled.getArgb(); - } - return enabled ? theme().textPrimary() : theme().textMuted(); + public NumberValue<Float> getGradientIntensity() { + return gradientIntensity; } - private static float clamp01(float value) { - return Math.max(0.0f, Math.min(1.0f, value)); + public RGBAColorValue getCustomBg() { + return customBg; } - private static float clamp(float value, float min, float max) { - return Math.max(min, Math.min(max, value)); + public RGBAColorValue getCustomBgHover() { + return customBgHover; } - private static List<TextRenderUtil.FlatPart> flattenLegacy(Component text, int defaultColor) { - Component converted = LegacyTextUtil.convertLegacyCodes(text); - return TextRenderUtil.flatten(converted, defaultColor); + public RGBAColorValue getCustomBgDisabled() { + return customBgDisabled; } - 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(); - } + public RGBAColorValue getCustomStroke() { + return customStroke; } - 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(); - } + public RGBAColorValue getCustomStrokeHover() { + return customStrokeHover; } - private static double measureHeight(TextRenderer tr, float scale) { - tr.begin(scale, true, false); - try { - return tr.getHeight(false); - } finally { - tr.end(); - } + public RGBAColorValue getCustomStrokeDisabled() { + return customStrokeDisabled; } - private static void markRounded(AbstractWidget widget, float radius) { - if (widget instanceof IRoundedHitbox rounded) { - rounded.lovisual$setRoundedHitbox(radius); - } + public RGBAColorValue getCustomText() { + return customText; } - private static int forceOpaque(int argb) { - return (argb & 0x00FFFFFF) | 0xFF000000; + public RGBAColorValue getCustomTextDisabled() { + return customTextDisabled; } - private 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); + public NumberValue<Float> getCustomRadius() { + return customRadius; } - public static void renderTooltip() { - BetterTooltips.renderTooltipWithContext(LAST_CTX); + public NumberValue<Float> getCustomSoftness() { + return customSoftness; + } + + public NumberValue<Float> getCustomStrokeThickness() { + return customStrokeThickness; + } + + public BooleanValue getCustomGradient() { + return customGradient; + } + + public RGBAColorValue getCustomGradientColor() { + return customGradientColor; + } + + public NumberValue<Float> getCustomGradientAngle() { + return customGradientAngle; + } + + public NumberValue<Float> getCustomGradientIntensity() { + return customGradientIntensity; + } + + public NumberValue<Float> getCustomTextScale() { + return customTextScale; + } + + public NumberValue<Float> getCustomTextPadX() { + return customTextPadX; } @Override @@ -964,70 +385,4 @@ public final class BetterButtons extends AbstractHudElement { return id; } } - - private interface RenderCall { - public void render(GuiGraphicsExtractor ctx); - } - - private record ButtonParallax(float yawDeg, - float pitchDeg, - float rollDeg, - float scale, - float shiftX, - float shiftY, - boolean active) { - private static final ButtonParallax NONE = new ButtonParallax(0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, false); - } - - private record ButtonCall(AbstractWidget widget, - Component text, - float hover, - boolean enabled, - boolean focused, - ScissorRect scissor) implements RenderCall { - @Override - public void render(GuiGraphicsExtractor ctx) { - BetterButtons.renderButton(ctx, widget, text, hover, enabled, focused); - } - } - - private record SliderCall(AbstractWidget widget, - float value, - boolean dragging, - float hover, - ScissorRect scissor) implements RenderCall { - @Override - public void render(GuiGraphicsExtractor ctx) { - BetterButtons.renderSlider(ctx, widget, value, dragging, hover); - } - } - - private record TextIconCall(SpriteIconButton widget, - boolean iconOnly, - float hover, - boolean enabled, - boolean focused, - ScissorRect scissor) implements RenderCall { - @Override - public void render(GuiGraphicsExtractor ctx) { - BetterButtons.renderTextIconButton(ctx, widget, iconOnly, hover, enabled, focused); - } - } - - private record CyclingCall(CycleButton<?> widget, - Identifier icon, - boolean showLabel, - float hover, - boolean enabled, - boolean focused, - ScissorRect scissor) implements RenderCall { - @Override - public void render(GuiGraphicsExtractor ctx) { - BetterButtons.renderCyclingButton(ctx, widget, icon, showLabel, hover, enabled, focused); - } - } - - private record ScissorRect(int x1, int y1, int x2, int y2) { - } } - diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonSliderRenderer.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonSliderRenderer.java new file mode 100644 index 0000000..54980c1 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonSliderRenderer.java @@ -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); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonSprites.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonSprites.java new file mode 100644 index 0000000..79956b2 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonSprites.java @@ -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); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonStyleRenderer.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonStyleRenderer.java new file mode 100644 index 0000000..a54830d --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonStyleRenderer.java @@ -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)); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonTextRenderer.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonTextRenderer.java new file mode 100644 index 0000000..67b2471 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/render/ButtonTextRenderer.java @@ -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(); + } + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/state/ButtonHoverState.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/state/ButtonHoverState.java new file mode 100644 index 0000000..bd2f128 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/state/ButtonHoverState.java @@ -0,0 +1,92 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.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); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/state/ButtonParallax.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/state/ButtonParallax.java new file mode 100644 index 0000000..3ae44a6 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/state/ButtonParallax.java @@ -0,0 +1,86 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.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); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/state/ButtonQueue.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/state/ButtonQueue.java new file mode 100644 index 0000000..3952a6a --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/buttons/state/ButtonQueue.java @@ -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) { + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/TabRichTextRenderer.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/TabRichTextRenderer.java index c0fa088..f341b0c 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/TabRichTextRenderer.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/TabRichTextRenderer.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/esp/ShitEsp.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/esp/ShitEsp.java new file mode 100644 index 0000000..dd92542 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/esp/ShitEsp.java @@ -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()); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/BetterMinecraft.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/vanilla/BetterMinecraft.java similarity index 93% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/BetterMinecraft.java rename to combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/vanilla/BetterMinecraft.java index 5a1d1bb..61b4080 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/BetterMinecraft.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/vanilla/BetterMinecraft.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/AutoTool.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/AutoTool.java index af3095b..4f20abe 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/AutoTool.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/AutoTool.java @@ -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)); + } } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/ClickPearl.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/ClickPearl.java index 93007ff..2a7d35a 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/ClickPearl.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/interact/ClickPearl.java @@ -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; - } } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/inventory/ShitDropper.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/inventory/ShitDropper.java deleted file mode 100644 index 99b9c7f..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/inventory/ShitDropper.java +++ /dev/null @@ -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()); - } - -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/Trails.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/Trails.java similarity index 99% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/Trails.java rename to combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/Trails.java index 8d1a3fa..d289b2e 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/Trails.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/Trails.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/WorldParticles.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/WorldParticles.java similarity index 77% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/WorldParticles.java rename to combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/WorldParticles.java index f007a38..ba8e7de 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/WorldParticles.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/WorldParticles.java @@ -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), diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/geometry/WorldParticlesGeometry.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/geometry/WorldParticlesGeometry.java new file mode 100644 index 0000000..c929c58 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/geometry/WorldParticlesGeometry.java @@ -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); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/geometry/WorldParticlesSphere.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/geometry/WorldParticlesSphere.java new file mode 100644 index 0000000..17c8cfb --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/ambient/geometry/WorldParticlesSphere.java @@ -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); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/HitBubbles.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/HitBubbles.java similarity index 99% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/HitBubbles.java rename to combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/HitBubbles.java index 94918a9..90fc682 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/HitBubbles.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/HitBubbles.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/HitEffect.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/HitEffect.java similarity index 99% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/HitEffect.java rename to combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/HitEffect.java index af3d71a..0394426 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/HitEffect.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/HitEffect.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/KillEffect.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/KillEffect.java similarity index 99% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/KillEffect.java rename to combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/KillEffect.java index 285000d..5f9d6c7 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/KillEffect.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/effects/hits/KillEffect.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/BlockESP.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/BlockESP.java index 1cecb5d..7504911 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/BlockESP.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/esp/overlay/BlockESP.java @@ -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(); } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/AlwaysVisibleHand.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/AlwaysVisibleHand.java similarity index 97% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/AlwaysVisibleHand.java rename to combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/AlwaysVisibleHand.java index 168ebaa..635cf1e 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/AlwaysVisibleHand.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/AlwaysVisibleHand.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/HoldMyItems.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/HoldMyItems.java similarity index 96% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/HoldMyItems.java rename to combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/HoldMyItems.java index df27839..6c841fd 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/HoldMyItems.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/HoldMyItems.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/optimization/FpsOptimizer.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/optimization/FpsOptimizer.java index 7d7b402..262fea0 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/optimization/FpsOptimizer.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/optimization/FpsOptimizer.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/Chams.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/Chams.java similarity index 99% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/Chams.java rename to combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/Chams.java index 987ea38..b210921 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/Chams.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/Chams.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/ReimaginedVisual.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/ReimaginedVisual.java similarity index 99% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/ReimaginedVisual.java rename to combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/ReimaginedVisual.java index 9cf8287..3b28a61 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/ReimaginedVisual.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/scene/visual/ReimaginedVisual.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/Rain.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/precip/Rain.java similarity index 99% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/Rain.java rename to combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/precip/Rain.java index e3520f8..c06b2e2 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/Rain.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/precip/Rain.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/WetWorld.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/precip/WetWorld.java similarity index 99% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/WetWorld.java rename to combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/precip/WetWorld.java index 04321db..dbd553e 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/WetWorld.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/precip/WetWorld.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/AtmoDawnFog.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/sky/AtmoDawnFog.java similarity index 99% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/AtmoDawnFog.java rename to combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/sky/AtmoDawnFog.java index ce42bd0..4331a34 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/AtmoDawnFog.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/sky/AtmoDawnFog.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/ShaderSky.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/sky/ShaderSky.java similarity index 99% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/ShaderSky.java rename to combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/sky/ShaderSky.java index f765b86..7e089c3 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/ShaderSky.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/weather/sky/ShaderSky.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/theme/Themes.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/theme/Themes.java index a25da8a..15b82a4 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/theme/Themes.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/theme/Themes.java @@ -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); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/theme/presets/ThemePresets1.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/theme/presets/ThemePresets1.java new file mode 100644 index 0000000..79e7c13 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/theme/presets/ThemePresets1.java @@ -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()) + ); +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/theme/presets/ThemePresets2.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/theme/presets/ThemePresets2.java new file mode 100644 index 0000000..bcbb604 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/theme/presets/ThemePresets2.java @@ -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) + ); +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/theme/presets/ThemePresets3.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/theme/presets/ThemePresets3.java new file mode 100644 index 0000000..128de20 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/theme/presets/ThemePresets3.java @@ -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) + ); +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/entity/inventory/PlayerInventoryMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/entity/inventory/PlayerInventoryMixin.java index 5413cfc..c3c2977 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/entity/inventory/PlayerInventoryMixin.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/entity/inventory/PlayerInventoryMixin.java @@ -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; - } } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/entity/living/LivingEntityMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/entity/living/LivingEntityMixin.java index df2da36..55a2377 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/entity/living/LivingEntityMixin.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/entity/living/LivingEntityMixin.java @@ -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. diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/core/AbstractContainerScreenMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/core/AbstractContainerScreenMixin.java index cd31264..e833a39 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/core/AbstractContainerScreenMixin.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/core/AbstractContainerScreenMixin.java @@ -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); + } + } } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/multiplayer/RecipeBookScreenMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/multiplayer/RecipeBookScreenMixin.java index 17e9a82..1f75deb 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/multiplayer/RecipeBookScreenMixin.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/gui/multiplayer/RecipeBookScreenMixin.java @@ -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 { diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/iris/hand/IrisVanillaHandInteropMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/iris/hand/IrisVanillaHandInteropMixin.java index 74b9ea2..e390e35 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/iris/hand/IrisVanillaHandInteropMixin.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/iris/hand/IrisVanillaHandInteropMixin.java @@ -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) diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/network/game/MultiPlayerGameModeMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/network/game/MultiPlayerGameModeMixin.java index 3503d8f..b937953 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/network/game/MultiPlayerGameModeMixin.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/network/game/MultiPlayerGameModeMixin.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/network/game/PlayerInfoMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/network/game/PlayerInfoMixin.java index fb5973e..eec97bf 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/network/game/PlayerInfoMixin.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/network/game/PlayerInfoMixin.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/entity/EndCrystalModelMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/entity/crystal/EndCrystalModelMixin.java similarity index 97% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/entity/EndCrystalModelMixin.java rename to combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/entity/crystal/EndCrystalModelMixin.java index beb7ff3..3ebf53c 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/entity/EndCrystalModelMixin.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/entity/crystal/EndCrystalModelMixin.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/entity/EndCrystalModelTintMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/entity/crystal/EndCrystalModelTintMixin.java similarity index 97% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/entity/EndCrystalModelTintMixin.java rename to combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/entity/crystal/EndCrystalModelTintMixin.java index d7d3cbd..153b212 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/entity/EndCrystalModelTintMixin.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/entity/crystal/EndCrystalModelTintMixin.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/entity/ItemEntityRenderStateMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/entity/item/ItemEntityRenderStateMixin.java similarity index 93% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/entity/ItemEntityRenderStateMixin.java rename to combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/entity/item/ItemEntityRenderStateMixin.java index d75350d..a43fe3c 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/entity/ItemEntityRenderStateMixin.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/entity/item/ItemEntityRenderStateMixin.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/entity/ItemEntityRendererMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/entity/item/ItemEntityRendererMixin.java similarity index 98% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/entity/ItemEntityRendererMixin.java rename to combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/entity/item/ItemEntityRendererMixin.java index 880caf4..f39a0e3 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/entity/ItemEntityRendererMixin.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/entity/item/ItemEntityRendererMixin.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/fog/AtmosphericFogEnvironmentMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/fog/AtmosphericFogEnvironmentMixin.java index 2be9a81..9e4b4c2 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/fog/AtmosphericFogEnvironmentMixin.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/fog/AtmosphericFogEnvironmentMixin.java @@ -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) diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/fog/FogRendererMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/fog/FogRendererMixin.java index a4237e3..a402a77 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/fog/FogRendererMixin.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/fog/FogRendererMixin.java @@ -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) diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/item/ItemInHandAlwaysVisibleMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/item/ItemInHandAlwaysVisibleMixin.java index 65a226f..79e4f45 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/item/ItemInHandAlwaysVisibleMixin.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/item/ItemInHandAlwaysVisibleMixin.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/item/ItemInHandRendererMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/item/ItemInHandRendererMixin.java index 2311f64..137711e 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/item/ItemInHandRendererMixin.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/item/ItemInHandRendererMixin.java @@ -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) diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/level/LevelRendererMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/level/LevelRendererMixin.java index 9e33dfc..941a852 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/level/LevelRendererMixin.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/level/LevelRendererMixin.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/level/FireworkParticlesStarterMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/level/particles/FireworkParticlesStarterMixin.java similarity index 98% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/level/FireworkParticlesStarterMixin.java rename to combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/level/particles/FireworkParticlesStarterMixin.java index c8fd554..b3cb19c 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/level/FireworkParticlesStarterMixin.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/level/particles/FireworkParticlesStarterMixin.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/level/ParticleManagerMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/level/particles/ParticleManagerMixin.java similarity index 91% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/level/ParticleManagerMixin.java rename to combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/level/particles/ParticleManagerMixin.java index 84b1509..1a6ef68 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/level/ParticleManagerMixin.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/level/particles/ParticleManagerMixin.java @@ -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) diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/main/GameRendererMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/main/GameRendererMixin.java index 602c5bc..f38bf5e 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/main/GameRendererMixin.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/main/GameRendererMixin.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/LoVisualRenderPipelines.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/LoVisualRenderPipelines.java index 3c6baf7..f7e74be 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/LoVisualRenderPipelines.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/LoVisualRenderPipelines.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/WorldPipelines.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/WorldPipelines.java index 085fde6..3c4c8f5 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/WorldPipelines.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/pipeline/pipelines/WorldPipelines.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/resource/PostProcessGraphResources.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/resource/PostProcessGraphResources.java index 0b4c3cd..ae825d7 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/resource/PostProcessGraphResources.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/resource/PostProcessGraphResources.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/render/SkyShaderRenderer.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/render/SkyShaderRenderer.java index 1e7b94b..8bec63c 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/render/SkyShaderRenderer.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/render/SkyShaderRenderer.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/render/SkyShaderStarryRenderer.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/render/SkyShaderStarryRenderer.java index 7c5973c..eaab000 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/render/SkyShaderStarryRenderer.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/render/SkyShaderStarryRenderer.java @@ -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; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/OrderedUiBatcher.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/OrderedUiBatcher.java index 44c89c9..f5830a5 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/OrderedUiBatcher.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/OrderedUiBatcher.java @@ -7,98 +7,50 @@ 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, - @Nullable GpuSampler sourceSampler, - 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(); + @Nullable GpuTextureView sourceView, + @Nullable GpuSampler sourceSampler, + float screenW, + float screenH, + float uiScale) { + 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) { + 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(); - } - } - } - - 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; - } - } + state.closeRetained(); } - int poolTotal() { - int total = 0; - for (ObjectArrayList<DrawBatch> pool : pools) { - total += pool.size(); + public void deactivate() { + active = false; + Renderer2D.BATCH_STATS.setActive(false); + } + + private void flushFailed(String reason, boolean finish) { + Renderer2D.BATCH_STATS.noteFailure(reason); + state.resetOrder(); + blur.reset(); + finishPhase(finish); + } + + private void finishPhase(boolean finish) { + if (finish) { + blur.reset(); + active = false; + Renderer2D.BATCH_STATS.setActive(false); } - return total + itemPool.size() + textPool.size(); } } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/LiquidGlassTextFlush.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/LiquidGlassTextFlush.java new file mode 100644 index 0000000..791216e --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/LiquidGlassTextFlush.java @@ -0,0 +1,82 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.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(); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/OrderedDeferral.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/OrderedDeferral.java new file mode 100644 index 0000000..a304a07 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/OrderedDeferral.java @@ -0,0 +1,68 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.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); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/OrderedDrawBatchFlush.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/OrderedDrawBatchFlush.java new file mode 100644 index 0000000..a125068 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/OrderedDrawBatchFlush.java @@ -0,0 +1,193 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.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); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/OrderedFlushRenderer.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/OrderedFlushRenderer.java new file mode 100644 index 0000000..ce2cf8a --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/flush/OrderedFlushRenderer.java @@ -0,0 +1,157 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.render.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; + } + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/order/DrawBatchPool.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/order/DrawBatchPool.java new file mode 100644 index 0000000..73e0125 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/order/DrawBatchPool.java @@ -0,0 +1,143 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.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(); + } + } + } + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/order/ItemTextPools.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/order/ItemTextPools.java new file mode 100644 index 0000000..d618408 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/order/ItemTextPools.java @@ -0,0 +1,130 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.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; + } + } + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/order/OrderState.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/order/OrderState.java new file mode 100644 index 0000000..f389302 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/batches/ordered/order/OrderState.java @@ -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(); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/kawase/KawaseBlurPreparer.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/kawase/KawaseBlurPreparer.java new file mode 100644 index 0000000..37783e6 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/kawase/KawaseBlurPreparer.java @@ -0,0 +1,145 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.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); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/kawase/KawasePass.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/kawase/KawasePass.java new file mode 100644 index 0000000..81396f0 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/kawase/KawasePass.java @@ -0,0 +1,64 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.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()); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/kawase/SharedKawaseBlur.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/kawase/SharedKawaseBlur.java new file mode 100644 index 0000000..ff5ddd3 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/kawase/SharedKawaseBlur.java @@ -0,0 +1,118 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.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); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/backend/SvgMeshBackend.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/backend/SvgMeshBackend.java index d10a0e5..6b7625c 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/backend/SvgMeshBackend.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/backend/SvgMeshBackend.java @@ -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() { } } } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/backend/geometry/SvgGeometry.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/backend/geometry/SvgGeometry.java new file mode 100644 index 0000000..46ce8d0 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/backend/geometry/SvgGeometry.java @@ -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; } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/backend/parser/SvgParser.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/backend/parser/SvgParser.java new file mode 100644 index 0000000..e387a6a --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/backend/parser/SvgParser.java @@ -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; + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/backend/render/SvgRender.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/backend/render/SvgRender.java new file mode 100644 index 0000000..4887490 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/backend/render/SvgRender.java @@ -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()); + } + }); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/backend/store/SvgCache.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/backend/store/SvgCache.java new file mode 100644 index 0000000..a8b636a --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/backend/store/SvgCache.java @@ -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(); } + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/backend/texture/SvgTexture.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/backend/texture/SvgTexture.java new file mode 100644 index 0000000..05769af --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/svg/backend/texture/SvgTexture.java @@ -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); } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/glyph/CustomTextRenderer.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/glyph/CustomTextRenderer.java index 0fea0f8..7a82eaf 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/glyph/CustomTextRenderer.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/glyph/CustomTextRenderer.java @@ -7,134 +7,48 @@ package dev.loki.lovisual.render.engine.text.glyph; -import dev.loki.lovisual.render.engine.text.backend.placement.TextPlacementMode; -import org.lwjgl.system.MemoryUtil; +import com.mojang.blaze3d.pipeline.RenderPipeline; import dev.loki.lovisual.render.engine.color.RenderColor; import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; -import dev.loki.lovisual.render.engine.uniform.MeshBuilder; -import dev.loki.lovisual.render.helpers.clip.ScissorFunction; - -import java.nio.ByteBuffer; +import dev.loki.lovisual.render.engine.text.backend.placement.TextPlacementMode; import dev.loki.lovisual.render.engine.text.font.Font; import dev.loki.lovisual.render.engine.text.font.FontFace; -import dev.loki.lovisual.render.engine.text.font.FontUtils; -import dev.loki.lovisual.render.engine.text.font.faces.BuiltinFontFace; -import dev.loki.lovisual.render.engine.text.font.faces.EmptyGlyphFont; -import dev.loki.lovisual.render.engine.text.font.info.FontDebugStats; -import dev.loki.lovisual.render.engine.text.msdf.MsdfFont; +import dev.loki.lovisual.render.engine.text.glyph.custom.CustomFontSet; import dev.loki.lovisual.render.engine.text.render.TextRenderSystem; import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; public class CustomTextRenderer implements TextRenderer { public static final RenderColor SHADOW_COLOR = new RenderColor(60, 60, 60, 180); public final FontFace fontFace; - private final MeshBuilder mesh = new MeshBuilder(LoVisualRenderPipelines.UI_TEXT); - private GlyphFont[] fonts; - private GlyphFont font; - private int fontIndex; - - private boolean building; - private boolean scaleOnly; - private boolean meshStarted; - private boolean liquidGlassText; - private double fontScale = 1; - private double scale = 1; + final MeshBuilder mesh = new MeshBuilder(LoVisualRenderPipelines.UI_TEXT); + final CustomFontSet fontSet; + GlyphFont font; + boolean building; + boolean meshStarted; + boolean liquidGlassText; + double fontScale = 1; + double scale = 1; public CustomTextRenderer(FontFace fontFace) { this.fontFace = fontFace; - - this.fonts = createFonts(); + this.fontSet = new CustomFontSet(fontFace); } - private static double fadeAlphaAt(double x, - double clipLeft, - double clipRight, - double fadeLeft, - double fadeRight) { - if (clipRight <= clipLeft) return 1.0; - if (x <= clipLeft || x >= clipRight) return 0.0; - - double alpha = 1.0; - if (fadeLeft > 0.0 && x < clipLeft + fadeLeft) { - alpha = Math.min(alpha, (x - clipLeft) / fadeLeft); - } - if (fadeRight > 0.0 && x > clipRight - fadeRight) { - alpha = Math.min(alpha, (clipRight - x) / fadeRight); - } - return clamp01(alpha); - } - - private static int scaleArgbAlpha(int argb, double factor) { - int a = (argb >>> 24) & 0xFF; - int scaled = (int) Math.round(a * clamp01(factor)); - return (argb & 0x00FFFFFF) | ((scaled & 0xFF) << 24); - } - - private static double clamp01(double value) { - return Math.max(0.0, Math.min(1.0, value)); - } - - private GlyphFont[] createFonts() { - boolean msdfEligible = fontFace instanceof BuiltinFontFace - && !FontUtils.isIconFamily(fontFace.info.family()); - if (msdfEligible) { - FontDebugStats.noteMsdfAttempt(fontFace.info); - GlyphFont[] msdf = MsdfFont.tryCreate(fontFace); - if (msdf != null) { - FontDebugStats.noteMsdfSuccess(fontFace.info); - return msdf; - } - FontDebugStats.noteMsdfFallback(fontFace.info, MsdfFont.getLastError()); - if (fontFace.isAtlasOnly()) { - return createEmptyFonts(); - } + /** + * Выбранный глиф-шрифт и производные масштабы; glyphScale/shadowOffset — + * производные акцессоры, формат старого рекорда сохранён. + */ + public record GlyphSelection(GlyphFont font, double fontScale, double scale) { + public double glyphScale() { + return scale / 1.5; } - byte[] bytes = FontUtils.readBytes(fontFace.toStream()); - ByteBuffer buffer = MemoryUtil.memAlloc(bytes.length); - try { - buffer.put(bytes).flip(); - - GlyphFont[] result = new GlyphFont[5]; - for (int i = 0; i < result.length; i++) { - result[i] = new Font(buffer, (int) Math.round(27 * ((i * 0.5) + 1))); - } - return result; - } finally { - MemoryUtil.memFree(buffer); + public double shadowOffset() { + return fontScale * glyphScale(); } } - private GlyphFont[] createEmptyFonts() { - GlyphFont[] result = new GlyphFont[5]; - for (int i = 0; i < result.length; i++) { - int height = (int) Math.round(27 * ((i * 0.5) + 1)); - result[i] = new EmptyGlyphFont(height); - } - return result; - } - - private boolean rebuildFonts() { - GlyphFont[] old = this.fonts; - GlyphFont[] fresh; - try { - fresh = createFonts(); - } catch (Exception e) { - return false; - } - - this.fonts = fresh; - int index = Math.max(0, Math.min(fontIndex, fresh.length - 1)); - this.font = fresh[index]; - - if (old != null) { - for (GlyphFont f : old) { - f.close(); - } - } - return true; - } - @Override public void setAlpha(double a) { mesh.alpha = a; @@ -144,258 +58,123 @@ public class CustomTextRenderer implements TextRenderer { public void begin(double scale, boolean scaleOnly, boolean big) { if (building) throw new RuntimeException("CustomTextRenderer.begin() called twice"); this.building = true; - this.scaleOnly = scaleOnly; this.meshStarted = false; - selectGlyphFont(scale, big); - } - - GlyphSelection selectGlyphFont(double requestedScale, boolean big) { - if (big) { - this.fontIndex = fonts.length - 1; - this.font = fonts[fontIndex]; - } else { - double scaleA = Math.floor(requestedScale * 10) / 10; - int scaleI; - if (scaleA >= 3) scaleI = 5; - else if (scaleA >= 2.5) scaleI = 4; - else if (scaleA >= 2) scaleI = 3; - else if (scaleA >= 1.5) scaleI = 2; - else scaleI = 1; - fontIndex = scaleI - 1; - font = fonts[fontIndex]; - } - - this.fontScale = font.height() / 27.0; - this.scale = 1 + (requestedScale - fontScale) / fontScale; - double glyphScale = this.scale / 1.5; - return new GlyphSelection(font, glyphScale, fontScale * glyphScale); + applySelection(fontSet.select(scale, big)); } public GlyphSelection prepareGlyphFont(double requestedScale, boolean big) { - - GlyphSelection selection = selectGlyphFont(requestedScale, big); - if (!selection.font().isReady() && rebuildFonts()) { - selection = selectGlyphFont(requestedScale, big); - } + GlyphSelection selection = fontSet.prepare(requestedScale, big); + applySelection(selection); return selection; } + private void applySelection(GlyphSelection selection) { + this.font = selection.font(); + this.fontScale = selection.fontScale(); + this.scale = selection.scale(); + } + @Override public double getWidth(String text, int length, boolean shadow) { if (text == null || text.isEmpty()) return 0; - GlyphFont activeFont = building ? this.font : fonts[0]; + GlyphFont activeFont = activeFont(); return (activeFont.getWidth(text, length) + (shadow ? 1 : 0)) * scale / 1.5; } @Override public double getHeight(boolean shadow) { - GlyphFont activeFont = building ? this.font : fonts[0]; + GlyphFont activeFont = activeFont(); return (activeFont.height() + 1 + (shadow ? 1 : 0)) * scale / 1.5; } @Override public boolean hasGlyph(int codePoint) { - GlyphFont activeFont = building && this.font != null ? this.font : fonts[0]; + GlyphFont activeFont = building && font != null ? font : fontSet.base(); return activeFont != null && activeFont.hasGlyph(codePoint); } + private GlyphFont activeFont() { + return building ? font : fontSet.base(); + } + + interface GlyphRun { + double run(); + } + + double autoRun(GlyphRun body) { + boolean wasBuilding = building; + if (!wasBuilding) begin(); + try { + return body.run(); + } finally { + if (!wasBuilding) end(); + } + } + + double shadowPass(RenderColor color, boolean alphaFromColor, GlyphRun shadowGlyphs, GlyphRun mainGlyphs) { + int preShadowA = SHADOW_COLOR.a; + SHADOW_COLOR.a = (int) ((alphaFromColor ? color.a / 255.0 : mesh.alpha) * preShadowA); + double ignored = shadowGlyphs.run(); + double width = mainGlyphs.run(); + SHADOW_COLOR.a = preShadowA; + return width; + } + @Override public double render(String text, double x, double y, RenderColor color, boolean shadow) { - boolean wasBuilding = building; - if (!wasBuilding) begin(); - ensureMeshStarted(); - - double width; - if (shadow) { - int preShadowA = SHADOW_COLOR.a; - SHADOW_COLOR.a = (int) (color.a / 255.0 * preShadowA); - - font.render(mesh, text, x + fontScale * scale / 1.5, y + fontScale * scale / 1.5, SHADOW_COLOR, scale / 1.5); - width = font.render(mesh, text, x, y, color, scale / 1.5); - - SHADOW_COLOR.a = preShadowA; - } else { - width = font.render(mesh, text, x, y, color, scale / 1.5); - } - - if (!wasBuilding) end(); - return width; + return autoRun(() -> { + ensureMeshStarted(); + double offset = fontScale * scale / 1.5; + if (shadow) { + return shadowPass(color, true, + () -> font.render(mesh, text, x + offset, y + offset, SHADOW_COLOR, scale / 1.5), + () -> font.render(mesh, text, x, y, color, scale / 1.5)); + } + return font.render(mesh, text, x, y, color, scale / 1.5); + }); } @Override public double renderLiquidGlass(String text, double x, double y, RenderColor color, boolean shadow) { - boolean wasBuilding = building; - if (!wasBuilding) begin(); - // The glyph mesh belongs to a single pipeline. Mixing regular and // liquid-glass glyphs inside one begin/end block would submit earlier // glyphs through the wrong shader, so callers should use a separate // text run for this effect. - if (meshStarted && !liquidGlassText) { + if (building && meshStarted && !liquidGlassText) { return render(text, x, y, color, shadow); } - - liquidGlassText = true; - ensureMeshStarted(); - - double width; - if (shadow) { - int preShadowA = SHADOW_COLOR.a; - SHADOW_COLOR.a = (int) (color.a / 255.0 * preShadowA); - - font.render(mesh, text, x + fontScale * scale / 1.5, y + fontScale * scale / 1.5, SHADOW_COLOR, scale / 1.5); - width = font.render(mesh, text, x, y, color, scale / 1.5); - - SHADOW_COLOR.a = preShadowA; - } else { - width = font.render(mesh, text, x, y, color, scale / 1.5); - } - - if (!wasBuilding) end(); - return width; + return autoRun(() -> { + liquidGlassText = true; + ensureMeshStarted(); + double offset = fontScale * scale / 1.5; + if (shadow) { + return shadowPass(color, true, + () -> font.render(mesh, text, x + offset, y + offset, SHADOW_COLOR, scale / 1.5), + () -> font.render(mesh, text, x, y, color, scale / 1.5)); + } + return font.render(mesh, text, x, y, color, scale / 1.5); + }); } public double renderGradient(String text, double x, double y, Font.GlyphGradient gradient, boolean shadow) { - boolean wasBuilding = building; - if (!wasBuilding) begin(); - ensureMeshStarted(); - - double width; - if (shadow) { - int preShadowA = SHADOW_COLOR.a; - SHADOW_COLOR.a = (int) (preShadowA * mesh.alpha); - - font.render(mesh, text, x + fontScale * scale / 1.5, y + fontScale * scale / 1.5, SHADOW_COLOR, scale / 1.5); - width = font.renderGradient(mesh, text, x, y, scale / 1.5, gradient); - - SHADOW_COLOR.a = preShadowA; - } else { - width = font.renderGradient(mesh, text, x, y, scale / 1.5, gradient); - } - - if (!wasBuilding) end(); - return width; + return GradientTexts.gradient(this, text, x, y, gradient, shadow); } @Override public double renderQuadGradient(String text, double x, double y, Font.GlyphQuadGradient gradient, boolean shadow) { - boolean wasBuilding = building; - if (!wasBuilding) begin(); - ensureMeshStarted(); - - double width; - if (shadow) { - int preShadowA = SHADOW_COLOR.a; - SHADOW_COLOR.a = (int) (preShadowA * mesh.alpha); - - font.render(mesh, text, x + fontScale * scale / 1.5, y + fontScale * scale / 1.5, SHADOW_COLOR, scale / 1.5); - width = font.renderQuadGradient(mesh, text, x, y, scale / 1.5, gradient); - - SHADOW_COLOR.a = preShadowA; - } else { - width = font.renderQuadGradient(mesh, text, x, y, scale / 1.5, gradient); - } - - if (!wasBuilding) end(); - return width; + return GradientTexts.quadGradient(this, text, x, y, gradient, shadow); } - public double renderHorizontalFade(String text, - double x, - double y, - RenderColor color, - double clipLeft, - double clipRight, - double fadeLeft, - double fadeRight, + public double renderHorizontalFade(String text, double x, double y, RenderColor color, + double clipLeft, double clipRight, double fadeLeft, double fadeRight, boolean shadow) { - boolean wasBuilding = building; - if (!wasBuilding) begin(); - ensureMeshStarted(); - - double glyphScale = scale / 1.5; - Font.GlyphGradient mainGradient = horizontalFadeGradient(color, clipLeft, clipRight, fadeLeft, fadeRight, glyphScale); - - double width; - if (shadow) { - int preShadowA = SHADOW_COLOR.a; - SHADOW_COLOR.a = (int) (preShadowA * mesh.alpha); - - Font.GlyphGradient shadowGradient = horizontalFadeGradient( - SHADOW_COLOR, clipLeft, clipRight, fadeLeft, fadeRight, glyphScale - ); - font.renderGradient(mesh, text, x + fontScale * glyphScale, y + fontScale * glyphScale, glyphScale, shadowGradient); - width = font.renderGradient(mesh, text, x, y, glyphScale, mainGradient); - - SHADOW_COLOR.a = preShadowA; - } else { - width = font.renderGradient(mesh, text, x, y, glyphScale, mainGradient); - } - - if (!wasBuilding) end(); - return width; + return GradientTexts.horizontalFade(this, text, x, y, color, clipLeft, clipRight, fadeLeft, fadeRight, shadow); } - public double renderHorizontalFadeClipped(String text, - double x, - double y, - RenderColor color, - double clipLeft, - double clipRight, - double fadeLeft, - double fadeRight, + public double renderHorizontalFadeClipped(String text, double x, double y, RenderColor color, + double clipLeft, double clipRight, double fadeLeft, double fadeRight, boolean shadow) { - boolean wasBuilding = building; - if (!wasBuilding) begin(); - - boolean clipped = false; - double width; - try { - double clipWidth = Math.max(0.0, clipRight - clipLeft); - double clipHeight = Math.max(0.0, getHeight(shadow)); - clipped = ScissorFunction.pushRaw( - (float) clipLeft, - (float) y, - (float) clipWidth, - (float) clipHeight - ); - width = renderHorizontalFade(text, x, y, color, clipLeft, clipRight, fadeLeft, fadeRight, shadow); - } finally { - if (clipped) { - ScissorFunction.pop(); - } - if (!wasBuilding) { - end(); - } - } - return width; - } - - private Font.GlyphGradient horizontalFadeGradient(RenderColor color, - double clipLeft, - double clipRight, - double fadeLeft, - double fadeRight, - double glyphScale) { - int argb = ((color.a & 0xFF) << 24) - | ((color.r & 0xFF) << 16) - | ((color.g & 0xFF) << 8) - | (color.b & 0xFF); - double leftFade = Math.max(0.0, fadeLeft); - double rightFade = Math.max(0.0, fadeRight); - - return (index, cp, glyphX, out) -> { - double glyphAdvance = glyphAdvance(cp) * glyphScale; - double leftAlpha = fadeAlphaAt(glyphX, clipLeft, clipRight, leftFade, rightFade); - double rightAlpha = fadeAlphaAt(glyphX + glyphAdvance, clipLeft, clipRight, leftFade, rightFade); - out[0] = scaleArgbAlpha(argb, leftAlpha); - out[1] = scaleArgbAlpha(argb, rightAlpha); - }; - } - - private double glyphAdvance(int codePoint) { - String glyph = new String(Character.toChars(codePoint)); - return font.getWidth(glyph, glyph.length()); + return GradientTexts.horizontalFadeClipped(this, text, x, y, color, clipLeft, clipRight, fadeLeft, fadeRight, shadow); } @Override @@ -409,30 +188,34 @@ public class CustomTextRenderer implements TextRenderer { if (meshStarted) { mesh.end(); - if (!font.isReady()) { - rebuildFonts(); + if (!font.isReady() && fontSet.rebuild()) { + applySelection(fontSet.select(scale * fontScale, false)); } if (font.isReady()) { - TextRenderSystem.submitGlyphMesh( - liquidGlassText ? "LoVisual UI Liquid Glass Text" : "LoVisual UI Text", - font, - mesh, - liquidGlassText - ? (font.isMsdf() ? LoVisualRenderPipelines.UI_TEXT_LIQUID_GLASS_MSDF : LoVisualRenderPipelines.UI_TEXT_LIQUID_GLASS) - : (font.isMsdf() ? LoVisualRenderPipelines.UI_TEXT_MSDF : LoVisualRenderPipelines.UI_TEXT), - TextPlacementMode.UI - ); + submitMesh(); } } building = false; - scaleOnly = false; meshStarted = false; liquidGlassText = false; scale = 1; } - private void ensureMeshStarted() { + private void submitMesh() { + RenderPipeline pipeline = liquidGlassText + ? (font.isMsdf() ? LoVisualRenderPipelines.UI_TEXT_LIQUID_GLASS_MSDF : LoVisualRenderPipelines.UI_TEXT_LIQUID_GLASS) + : (font.isMsdf() ? LoVisualRenderPipelines.UI_TEXT_MSDF : LoVisualRenderPipelines.UI_TEXT); + TextRenderSystem.submitGlyphMesh( + liquidGlassText ? "LoVisual UI Liquid Glass Text" : "LoVisual UI Text", + font, + mesh, + pipeline, + TextPlacementMode.UI + ); + } + + void ensureMeshStarted() { if (meshStarted) { return; } @@ -443,18 +226,10 @@ public class CustomTextRenderer implements TextRenderer { mesh.begin(); } meshStarted = true; - scaleOnly = false; } public void destroy() { mesh.close(); - if (fonts == null) return; - for (GlyphFont f : this.fonts) { - f.close(); - } - } - - public record GlyphSelection(GlyphFont font, double glyphScale, double shadowOffset) { - + fontSet.close(); } } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/glyph/GradientTexts.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/glyph/GradientTexts.java new file mode 100644 index 0000000..88de745 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/glyph/GradientTexts.java @@ -0,0 +1,95 @@ +/* + * 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.text.glyph; + +import dev.loki.lovisual.render.engine.color.RenderColor; +import dev.loki.lovisual.render.engine.text.font.Font; +import dev.loki.lovisual.render.engine.text.glyph.custom.HorizontalFade; +import dev.loki.lovisual.render.helpers.clip.ScissorFunction; + +/** + * Градиентные текстовые прогоны {@link CustomTextRenderer}: линейный градиент, + * квад-градиент и горизонтальный fade (опционально под scissor-клипом). + * Тела 1:1 с прежними методами CustomTextRenderer. + */ +final class GradientTexts { + private GradientTexts() { + } + + static double gradient(CustomTextRenderer r, String text, double x, double y, + Font.GlyphGradient gradient, boolean shadow) { + return r.autoRun(() -> { + r.ensureMeshStarted(); + if (shadow) { + return r.shadowPass(null, false, + shadowGlyphs(r, text, x, y), + () -> r.font.renderGradient(r.mesh, text, x, y, r.scale / 1.5, gradient)); + } + return r.font.renderGradient(r.mesh, text, x, y, r.scale / 1.5, gradient); + }); + } + + static double quadGradient(CustomTextRenderer r, String text, double x, double y, + Font.GlyphQuadGradient gradient, boolean shadow) { + return r.autoRun(() -> { + r.ensureMeshStarted(); + if (shadow) { + return r.shadowPass(null, false, + shadowGlyphs(r, text, x, y), + () -> r.font.renderQuadGradient(r.mesh, text, x, y, r.scale / 1.5, gradient)); + } + return r.font.renderQuadGradient(r.mesh, text, x, y, r.scale / 1.5, gradient); + }); + } + + static double horizontalFade(CustomTextRenderer r, String text, double x, double y, RenderColor color, + double clipLeft, double clipRight, double fadeLeft, double fadeRight, + boolean shadow) { + return r.autoRun(() -> { + r.ensureMeshStarted(); + double glyphScale = r.scale / 1.5; + Font.GlyphGradient mainGradient = + HorizontalFade.gradient(r.font, color, clipLeft, clipRight, fadeLeft, fadeRight, glyphScale); + if (shadow) { + Font.GlyphGradient shadowGradient = + HorizontalFade.gradient(r.font, CustomTextRenderer.SHADOW_COLOR, clipLeft, clipRight, fadeLeft, fadeRight, glyphScale); + return r.shadowPass(color, false, + () -> r.font.renderGradient(r.mesh, text, x + r.fontScale * glyphScale, y + r.fontScale * glyphScale, glyphScale, shadowGradient), + () -> r.font.renderGradient(r.mesh, text, x, y, glyphScale, mainGradient)); + } + return r.font.renderGradient(r.mesh, text, x, y, glyphScale, mainGradient); + }); + } + + static double horizontalFadeClipped(CustomTextRenderer r, String text, double x, double y, RenderColor color, + double clipLeft, double clipRight, double fadeLeft, double fadeRight, + boolean shadow) { + boolean wasBuilding = r.building; + if (!wasBuilding) r.begin(); + boolean clipped = false; + try { + double clipWidth = Math.max(0.0, clipRight - clipLeft); + double clipHeight = Math.max(0.0, r.getHeight(shadow)); + clipped = ScissorFunction.pushRaw((float) clipLeft, (float) y, (float) clipWidth, (float) clipHeight); + return horizontalFade(r, text, x, y, color, clipLeft, clipRight, fadeLeft, fadeRight, shadow); + } finally { + if (clipped) { + ScissorFunction.pop(); + } + if (!wasBuilding) { + r.end(); + } + } + } + + private static CustomTextRenderer.GlyphRun shadowGlyphs(CustomTextRenderer r, String text, double x, double y) { + double offset = r.fontScale * r.scale / 1.5; + double glyphScale = r.scale / 1.5; + return () -> r.font.render(r.mesh, text, x + offset, y + offset, CustomTextRenderer.SHADOW_COLOR, glyphScale); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/glyph/custom/CustomFontSet.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/glyph/custom/CustomFontSet.java new file mode 100644 index 0000000..1ae9e73 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/glyph/custom/CustomFontSet.java @@ -0,0 +1,142 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.render.engine.text.glyph.custom; + +import dev.loki.lovisual.render.engine.text.font.Font; +import dev.loki.lovisual.render.engine.text.font.FontFace; +import dev.loki.lovisual.render.engine.text.font.FontUtils; +import dev.loki.lovisual.render.engine.text.font.faces.BuiltinFontFace; +import dev.loki.lovisual.render.engine.text.font.faces.EmptyGlyphFont; +import dev.loki.lovisual.render.engine.text.font.info.FontDebugStats; +import dev.loki.lovisual.render.engine.text.glyph.CustomTextRenderer.GlyphSelection; +import dev.loki.lovisual.render.engine.text.glyph.GlyphFont; +import dev.loki.lovisual.render.engine.text.msdf.MsdfFont; +import org.lwjgl.system.MemoryUtil; + +import java.nio.ByteBuffer; + +/** + * Набор размеров GlyphFont одного FontFace: MSDF для builtin-шрифтов, иначе + * растровый Font из TTF-байтов. Владеет выбором размера и пересозданием. + */ +public final class CustomFontSet { + private final FontFace fontFace; + private GlyphFont[] fonts; + private int fontIndex; + + public CustomFontSet(FontFace fontFace) { + this.fontFace = fontFace; + this.fonts = createFonts(); + } + + public GlyphFont current() { + return fontIndex >= 0 && fontIndex < fonts.length ? fonts[fontIndex] : null; + } + + public GlyphFont base() { + return fonts[0]; + } + + public GlyphSelection select(double requestedScale, boolean big) { + if (big) { + fontIndex = fonts.length - 1; + } else { + double scaleA = Math.floor(requestedScale * 10) / 10; + int scaleI; + if (scaleA >= 3) scaleI = 5; + else if (scaleA >= 2.5) scaleI = 4; + else if (scaleA >= 2) scaleI = 3; + else if (scaleA >= 1.5) scaleI = 2; + else scaleI = 1; + fontIndex = scaleI - 1; + } + + double fontScale = fonts[fontIndex].height() / 27.0; + double scale = 1 + (requestedScale - fontScale) / fontScale; + return new GlyphSelection(fonts[fontIndex], fontScale, scale); + } + + /** + * Выбор с одним retry: если выбранный шрифт не готов (атлас не догрузился) — + * пересоздаёт набор и выбирает заново. + */ + public GlyphSelection prepare(double requestedScale, boolean big) { + GlyphSelection selection = select(requestedScale, big); + if (!selection.font().isReady() && rebuild()) { + selection = select(requestedScale, big); + } + return selection; + } + + public boolean rebuild() { + GlyphFont[] old = this.fonts; + GlyphFont[] fresh; + try { + fresh = createFonts(); + } catch (Exception e) { + return false; + } + + this.fonts = fresh; + this.fontIndex = Math.max(0, Math.min(fontIndex, fresh.length - 1)); + + if (old != null) { + for (GlyphFont f : old) { + f.close(); + } + } + return true; + } + + public void close() { + if (fonts == null) return; + for (GlyphFont f : fonts) { + f.close(); + } + } + + private GlyphFont[] createFonts() { + boolean msdfEligible = fontFace instanceof BuiltinFontFace + && !FontUtils.isIconFamily(fontFace.info.family()); + if (msdfEligible) { + FontDebugStats.noteMsdfAttempt(fontFace.info); + GlyphFont[] msdf = MsdfFont.tryCreate(fontFace); + if (msdf != null) { + FontDebugStats.noteMsdfSuccess(fontFace.info); + return msdf; + } + FontDebugStats.noteMsdfFallback(fontFace.info, MsdfFont.getLastError()); + if (fontFace.isAtlasOnly()) { + return createEmptyFonts(); + } + } + + byte[] bytes = FontUtils.readBytes(fontFace.toStream()); + ByteBuffer buffer = MemoryUtil.memAlloc(bytes.length); + try { + buffer.put(bytes).flip(); + + GlyphFont[] result = new GlyphFont[5]; + for (int i = 0; i < result.length; i++) { + result[i] = new Font(buffer, (int) Math.round(27 * ((i * 0.5) + 1))); + } + return result; + } finally { + MemoryUtil.memFree(buffer); + } + } + + private static GlyphFont[] createEmptyFonts() { + GlyphFont[] result = new GlyphFont[5]; + for (int i = 0; i < result.length; i++) { + int height = (int) Math.round(27 * ((i * 0.5) + 1)); + result[i] = new EmptyGlyphFont(height); + } + return result; + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/glyph/custom/HorizontalFade.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/glyph/custom/HorizontalFade.java new file mode 100644 index 0000000..eb19bf8 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/glyph/custom/HorizontalFade.java @@ -0,0 +1,77 @@ +/* + * 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.text.glyph.custom; + +import dev.loki.lovisual.render.engine.color.RenderColor; +import dev.loki.lovisual.render.engine.text.font.Font; +import dev.loki.lovisual.render.engine.text.glyph.GlyphFont; + +/** + * Горизонтальный fade-градиент текста: альфа каждого края глифа плавно + * гаснет у границ клипа [clipLeft, clipRight] с шириной зоны fade. + */ +public final class HorizontalFade { + private HorizontalFade() { + } + + public static Font.GlyphGradient gradient(GlyphFont font, + RenderColor color, + double clipLeft, + double clipRight, + double fadeLeft, + double fadeRight, + double glyphScale) { + int argb = ((color.a & 0xFF) << 24) + | ((color.r & 0xFF) << 16) + | ((color.g & 0xFF) << 8) + | (color.b & 0xFF); + double leftFade = Math.max(0.0, fadeLeft); + double rightFade = Math.max(0.0, fadeRight); + + return (index, cp, glyphX, out) -> { + double glyphAdvance = glyphAdvance(font, cp) * glyphScale; + double leftAlpha = fadeAlphaAt(glyphX, clipLeft, clipRight, leftFade, rightFade); + double rightAlpha = fadeAlphaAt(glyphX + glyphAdvance, clipLeft, clipRight, leftFade, rightFade); + out[0] = scaleArgbAlpha(argb, leftAlpha); + out[1] = scaleArgbAlpha(argb, rightAlpha); + }; + } + + private static double glyphAdvance(GlyphFont font, int codePoint) { + String glyph = new String(Character.toChars(codePoint)); + return font.getWidth(glyph, glyph.length()); + } + + private static double fadeAlphaAt(double x, + double clipLeft, + double clipRight, + double fadeLeft, + double fadeRight) { + if (clipRight <= clipLeft) return 1.0; + if (x <= clipLeft || x >= clipRight) return 0.0; + + double alpha = 1.0; + if (fadeLeft > 0.0 && x < clipLeft + fadeLeft) { + alpha = Math.min(alpha, (x - clipLeft) / fadeLeft); + } + if (fadeRight > 0.0 && x > clipRight - fadeRight) { + alpha = Math.min(alpha, (clipRight - x) / fadeRight); + } + return clamp01(alpha); + } + + private static int scaleArgbAlpha(int argb, double factor) { + int a = (argb >>> 24) & 0xFF; + int scaled = (int) Math.round(a * clamp01(factor)); + return (argb & 0x00FFFFFF) | ((scaled & 0xFF) << 24); + } + + private static double clamp01(double value) { + return Math.max(0.0, Math.min(1.0, value)); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/glyph/TextGlyphFallback.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/glyph/custom/TextGlyphFallback.java similarity index 96% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/glyph/TextGlyphFallback.java rename to combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/glyph/custom/TextGlyphFallback.java index 151a264..431ddf1 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/glyph/TextGlyphFallback.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/glyph/custom/TextGlyphFallback.java @@ -5,11 +5,12 @@ * Licensed under the GNU General Public License v3.0. */ -package dev.loki.lovisual.render.engine.text.glyph; +package dev.loki.lovisual.render.engine.text.glyph.custom; import java.util.Set; import dev.loki.lovisual.render.engine.text.font.Fonts; import dev.loki.lovisual.render.engine.text.font.info.FontInfo; +import dev.loki.lovisual.render.engine.text.glyph.VanillaTextRenderer; import dev.loki.lovisual.render.engine.text.render.TextRenderer; /** diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/msdf/MsdfFont.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/msdf/MsdfFont.java index 8eb56e2..27e60b6 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/msdf/MsdfFont.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/msdf/MsdfFont.java @@ -7,41 +7,28 @@ package dev.loki.lovisual.render.engine.text.msdf; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.mojang.blaze3d.GpuFormat; -import com.mojang.blaze3d.textures.FilterMode; -import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -import net.minecraft.client.Minecraft; -import net.minecraft.resources.Identifier; -import net.minecraft.server.packs.resources.ResourceManager; -import org.lwjgl.system.MemoryUtil; -import dev.loki.lovisual.render.engine.texture.Texture; import dev.loki.lovisual.render.engine.color.RenderColor; -import dev.loki.lovisual.render.engine.uniform.MeshBuilder; - -import javax.imageio.ImageIO; -import java.awt.image.BufferedImage; -import java.io.InputStream; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.util.LinkedHashMap; -import java.util.Map; import dev.loki.lovisual.render.engine.text.font.Font; import dev.loki.lovisual.render.engine.text.font.FontFace; import dev.loki.lovisual.render.engine.text.font.FontUtils; import dev.loki.lovisual.render.engine.text.font.faces.BuiltinFontFace; -import dev.loki.lovisual.render.engine.text.font.info.FontInfo; 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.msdf.atlas.MsdfAtlas; +import dev.loki.lovisual.render.engine.text.msdf.atlas.MsdfGlyph; +import dev.loki.lovisual.render.engine.texture.Texture; +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * MSDF-шрифтовый фасад GlyphFont: размеры от одного атласа, LRU-кэш ширин. + * Загрузка — {@link MsdfAtlas}, генерация квадов — {@link MsdfGlyphDrawer}. + */ public final class MsdfFont implements GlyphFont { private static final int FONT_SIZES = 5; private static final float BASE_HEIGHT = 27f; private static final int WIDTH_CACHE_LIMIT = 512; - private static volatile String lastError = ""; private final MsdfAtlas atlas; private final float pixelScale; private final int height; @@ -75,11 +62,7 @@ public final class MsdfFont implements GlyphFont { } public static String getLastError() { - return lastError; - } - - private static void setLastError(String reason) { - lastError = reason != null ? reason : ""; + return MsdfAtlas.lastError(); } @Override @@ -103,9 +86,9 @@ public final class MsdfFont implements GlyphFont { double width = 0; for (int i = 0; i < safeLength; ) { int cp = string.codePointAt(i); - Glyph g = atlas.glyphs.get(cp); + MsdfGlyph g = atlas.glyphs.get(cp); if (g == null) g = atlas.fallback; - if (g != null) width += g.advance * pixelScale; + if (g != null) width += g.advance() * pixelScale; i += Character.charCount(cp); } widthCache.put(key, width); @@ -119,71 +102,12 @@ public final class MsdfFont implements GlyphFont { @Override public double render(MeshBuilder mesh, String string, double x, double y, RenderColor color, double scale) { - double baseline = y + atlas.ascender * pixelScale * scale; - - int length = string.length(); - mesh.ensureCapacity(length * 4, length * 6); - - for (int i = 0; i < length; ) { - int cp = string.codePointAt(i); - Glyph g = atlas.glyphs.get(cp); - if (g == null) g = atlas.fallback; - if (g == null) { - i += Character.charCount(cp); - continue; - } - - if (g.visible) { - double scalePx = pixelScale * scale; - double x0 = x + g.left * scalePx; - double x1 = x + g.right * scalePx; - double y0 = baseline + g.top * scalePx; - double y1 = baseline + g.bottom * scalePx; - - int i1 = mesh.vec2(x0, y0).raw2(g.u0, g.v0).color(color.r, color.g, color.b, color.a).next(); - int i2 = mesh.vec2(x0, y1).raw2(g.u0, g.v1).color(color.r, color.g, color.b, color.a).next(); - int i3 = mesh.vec2(x1, y1).raw2(g.u1, g.v1).color(color.r, color.g, color.b, color.a).next(); - int i4 = mesh.vec2(x1, y0).raw2(g.u1, g.v0).color(color.r, color.g, color.b, color.a).next(); - - mesh.quad(i1, i2, i3, i4); - } - - x += g.advance * pixelScale * scale; - i += Character.charCount(cp); - } - - return x; + return MsdfGlyphDrawer.render(atlas, pixelScale, mesh, string, x, y, color, scale); } @Override public double emitGlyphs(String string, double x, double y, double scale, GlyphConsumer consumer) { - double baseline = y + atlas.ascender * pixelScale * scale; - - int length = string.length(); - for (int i = 0; i < length; ) { - int cp = string.codePointAt(i); - Glyph g = atlas.glyphs.get(cp); - if (g == null) g = atlas.fallback; - if (g == null) { - i += Character.charCount(cp); - continue; - } - - if (g.visible) { - double scalePx = pixelScale * scale; - double x0 = x + g.left * scalePx; - double x1 = x + g.right * scalePx; - double y0 = baseline + g.top * scalePx; - double y1 = baseline + g.bottom * scalePx; - - consumer.accept(x0, y0, x1, y1, g.u0, g.v0, g.u1, g.v1); - } - - x += g.advance * pixelScale * scale; - i += Character.charCount(cp); - } - - return x; + return MsdfGlyphDrawer.emitGlyphs(atlas, pixelScale, string, x, y, scale, consumer); } @Override @@ -193,112 +117,18 @@ public final class MsdfFont implements GlyphFont { @Override public double getAdvance(int codePoint) { - Glyph g = atlas.glyphs.get(codePoint); - return g != null ? g.advance * pixelScale : 0.0; + MsdfGlyph g = atlas.glyphs.get(codePoint); + return g != null ? g.advance() * pixelScale : 0.0; } @Override public double renderGradient(MeshBuilder mesh, String string, double x, double y, double scale, Font.GlyphGradient gradient) { - double baseline = y + atlas.ascender * pixelScale * scale; - - int length = string.length(); - mesh.ensureCapacity(length * 4, length * 6); - - int[] colors = new int[2]; - int glyphIndex = 0; - for (int i = 0; i < length; ) { - int cp = string.codePointAt(i); - Glyph g = atlas.glyphs.get(cp); - if (g == null) g = atlas.fallback; - if (g == null) { - i += Character.charCount(cp); - glyphIndex++; - continue; - } - - gradient.colors(glyphIndex, cp, x, colors); - int leftArgb = colors[0]; - int rightArgb = colors[1]; - - int la = (leftArgb >>> 24) & 0xFF; - int lr = (leftArgb >>> 16) & 0xFF; - int lg = (leftArgb >>> 8) & 0xFF; - int lb = leftArgb & 0xFF; - - int ra = (rightArgb >>> 24) & 0xFF; - int rr = (rightArgb >>> 16) & 0xFF; - int rg = (rightArgb >>> 8) & 0xFF; - int rb = rightArgb & 0xFF; - - if (g.visible) { - double scalePx = pixelScale * scale; - double x0 = x + g.left * scalePx; - double x1 = x + g.right * scalePx; - double y0 = baseline + g.top * scalePx; - double y1 = baseline + g.bottom * scalePx; - - int i1 = mesh.vec2(x0, y0).raw2(g.u0, g.v0).color(lr, lg, lb, la).next(); - int i2 = mesh.vec2(x0, y1).raw2(g.u0, g.v1).color(lr, lg, lb, la).next(); - int i3 = mesh.vec2(x1, y1).raw2(g.u1, g.v1).color(rr, rg, rb, ra).next(); - int i4 = mesh.vec2(x1, y0).raw2(g.u1, g.v0).color(rr, rg, rb, ra).next(); - - mesh.quad(i1, i2, i3, i4); - } - - x += g.advance * pixelScale * scale; - i += Character.charCount(cp); - glyphIndex++; - } - - return x; + return MsdfGlyphDrawer.renderGradient(atlas, pixelScale, mesh, string, x, y, scale, gradient); } @Override public double renderQuadGradient(MeshBuilder mesh, String string, double x, double y, double scale, Font.GlyphQuadGradient gradient) { - double baseline = y + atlas.ascender * pixelScale * scale; - - int length = string.length(); - mesh.ensureCapacity(length * 4, length * 6); - - int[] colors = new int[4]; - int glyphIndex = 0; - for (int i = 0; i < length; ) { - int cp = string.codePointAt(i); - Glyph g = atlas.glyphs.get(cp); - if (g == null) g = atlas.fallback; - if (g == null) { - i += Character.charCount(cp); - glyphIndex++; - continue; - } - - if (g.visible) { - double scalePx = pixelScale * scale; - double x0 = x + g.left * scalePx; - double x1 = x + g.right * scalePx; - double y0 = baseline + g.top * scalePx; - double y1 = baseline + g.bottom * scalePx; - - gradient.colors(glyphIndex, cp, x0, y0, x1, y1, colors); - int topLeftArgb = colors[0]; - int bottomLeftArgb = colors[1]; - int bottomRightArgb = colors[2]; - int topRightArgb = colors[3]; - - int i1 = mesh.vec2(x0, y0).raw2(g.u0, g.v0).color(red(topLeftArgb), green(topLeftArgb), blue(topLeftArgb), alpha(topLeftArgb)).next(); - int i2 = mesh.vec2(x0, y1).raw2(g.u0, g.v1).color(red(bottomLeftArgb), green(bottomLeftArgb), blue(bottomLeftArgb), alpha(bottomLeftArgb)).next(); - int i3 = mesh.vec2(x1, y1).raw2(g.u1, g.v1).color(red(bottomRightArgb), green(bottomRightArgb), blue(bottomRightArgb), alpha(bottomRightArgb)).next(); - int i4 = mesh.vec2(x1, y0).raw2(g.u1, g.v0).color(red(topRightArgb), green(topRightArgb), blue(topRightArgb), alpha(topRightArgb)).next(); - - mesh.quad(i1, i2, i3, i4); - } - - x += g.advance * pixelScale * scale; - i += Character.charCount(cp); - glyphIndex++; - } - - return x; + return MsdfGlyphDrawer.renderQuadGradient(atlas, pixelScale, mesh, string, x, y, scale, gradient); } @Override @@ -327,287 +157,6 @@ public final class MsdfFont implements GlyphFont { return atlas.height; } - private static int alpha(int argb) { - return (argb >>> 24) & 0xFF; - } - - private static int red(int argb) { - return (argb >>> 16) & 0xFF; - } - - private static int green(int argb) { - return (argb >>> 8) & 0xFF; - } - - private static int blue(int argb) { - return argb & 0xFF; - } - private record WidthKey(String text, int length) { } - - private static final class MsdfAtlas { - private final Texture texture; - private final float distanceRange; - private final int width; - private final int height; - private final float lineHeight; - private final float ascender; - private final Int2ObjectOpenHashMap<Glyph> glyphs; - private final Glyph fallback; - private int refs; - - private MsdfAtlas(Texture texture, - float distanceRange, - int width, - int height, - float lineHeight, - float ascender, - Int2ObjectOpenHashMap<Glyph> glyphs, - Glyph fallback) { - this.texture = texture; - this.distanceRange = distanceRange; - this.width = width; - this.height = height; - this.lineHeight = lineHeight; - this.ascender = ascender; - this.glyphs = glyphs; - this.fallback = fallback; - } - - private static MsdfAtlas load(FontInfo info) { - Minecraft mc = Minecraft.getInstance(); - if (mc == null) { - setLastError("mc null"); - return null; - } - ResourceManager rm = mc.getResourceManager(); - - Identifier jsonId = FontUtils.msdfResource(info, "json"); - Identifier pngId = FontUtils.msdfResource(info, "png"); - if (jsonId == null || pngId == null) { - setLastError("msdf ids missing"); - return null; - } - - JsonObject root; - try (InputStream in = rm.getResource(jsonId).orElseThrow().open()) { - String json = new String(in.readAllBytes(), StandardCharsets.UTF_8); - root = JsonParser.parseString(json).getAsJsonObject(); - } catch (Exception ignored) { - setLastError("msdf json missing/invalid: " + jsonId); - return null; - } - - BufferedImage image; - try (InputStream in = rm.getResource(pngId).orElseThrow().open()) { - image = ImageIO.read(in); - if (image == null) return null; - } catch (Exception ignored) { - setLastError("msdf png missing/invalid: " + pngId); - return null; - } - - JsonObject atlasObj = root.has("atlas") ? root.getAsJsonObject("atlas") : null; - float distanceRange = getFloat(atlasObj, "distanceRange", 4f); - int atlasWidth = getInt(atlasObj, "width", image.getWidth()); - int atlasHeight = getInt(atlasObj, "height", image.getHeight()); - if (atlasWidth <= 0) atlasWidth = image.getWidth(); - if (atlasHeight <= 0) atlasHeight = image.getHeight(); - String yOrigin = getString(atlasObj, "yOrigin", "bottom"); - boolean yOriginBottom = !"top".equalsIgnoreCase(yOrigin); - - JsonObject metricsObj = root.has("metrics") ? root.getAsJsonObject("metrics") : null; - float emSize = getFloat(metricsObj, "emSize", 1f); - float lineHeight = getFloat(metricsObj, "lineHeight", emSize); - float ascender = getFloat(metricsObj, "ascender", lineHeight); - if (ascender < 0f) ascender = -ascender; - if (lineHeight <= 0f) lineHeight = 1f; - - Int2ObjectOpenHashMap<Glyph> glyphs = new Int2ObjectOpenHashMap<>(); - JsonArray glyphArray = root.has("glyphs") ? root.getAsJsonArray("glyphs") : null; - if (glyphArray != null) { - float invW = 1f / atlasWidth; - float invH = 1f / atlasHeight; - - for (JsonElement el : glyphArray) { - if (!el.isJsonObject()) continue; - JsonObject g = el.getAsJsonObject(); - - int codePoint = readUnicode(g.get("unicode")); - if (codePoint < 0) continue; - - float advance = getFloat(g, "advance", 0f); - Bounds plane = readBounds(g.has("planeBounds") ? g.getAsJsonObject("planeBounds") : null); - Bounds atlas = readBounds(g.has("atlasBounds") ? g.getAsJsonObject("atlasBounds") : null); - - Glyph glyph = Glyph.from(advance, plane, atlas, yOriginBottom, atlasWidth, atlasHeight, invW, invH); - glyphs.put(codePoint, glyph); - } - } - - Glyph fallback = glyphs.get(32); - if (fallback == null) fallback = glyphs.get('?'); - - Texture texture = uploadTexture(image); - if (texture == null) { - setLastError("msdf texture upload failed: " + pngId); - return null; - } - - if (glyphs.isEmpty()) { - texture.close(); - setLastError("msdf glyphs empty: " + jsonId); - return null; - } - - setLastError(""); - TextRenderSystem.glyphAtlases().registerPage("msdf:" + jsonId, texture, atlasWidth, atlasHeight, true); - TextRenderSystem.glyphAtlases().onDirtyRectUpload(glyphs.size(), atlasWidth * atlasHeight * 4); - return new MsdfAtlas(texture, distanceRange, atlasWidth, atlasHeight, lineHeight, ascender, glyphs, fallback); - } - - private static 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); - - ByteBuffer buffer = MemoryUtil.memAlloc(w * h * 4); - 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 texture = new Texture(w, h, GpuFormat.RGBA8_UNORM, FilterMode.LINEAR, FilterMode.LINEAR); - texture.upload(buffer); - return texture; - } catch (Exception ignored) { - return null; - } finally { - MemoryUtil.memFree(buffer); - } - } - - private static float getFloat(JsonObject obj, String key, float fallback) { - if (obj == null || key == null || !obj.has(key)) return fallback; - try { - return obj.get(key).getAsFloat(); - } catch (Exception ignored) { - return fallback; - } - } - - private static int getInt(JsonObject obj, String key, int fallback) { - if (obj == null || key == null || !obj.has(key)) return fallback; - try { - return obj.get(key).getAsInt(); - } catch (Exception ignored) { - return fallback; - } - } - - private static String getString(JsonObject obj, String key, String fallback) { - if (obj == null || key == null || !obj.has(key)) return fallback; - try { - return obj.get(key).getAsString(); - } catch (Exception ignored) { - return fallback; - } - } - - private static int readUnicode(JsonElement element) { - if (element == null) return -1; - try { - if (element.isJsonPrimitive()) { - var prim = element.getAsJsonPrimitive(); - if (prim.isNumber()) return prim.getAsInt(); - if (prim.isString()) { - String str = prim.getAsString(); - if (str.isEmpty()) return -1; - if (str.length() == 1) return str.codePointAt(0); - if (str.startsWith("0x") || str.startsWith("0X")) { - return Integer.parseInt(str.substring(2), 16); - } - return Integer.parseInt(str); - } - } - } catch (Exception ignored) { - } - return -1; - } - - private static Bounds readBounds(JsonObject obj) { - if (obj == null) return null; - float left = getFloat(obj, "left", 0f); - float right = getFloat(obj, "right", 0f); - float top = getFloat(obj, "top", 0f); - float bottom = getFloat(obj, "bottom", 0f); - return new Bounds(left, bottom, right, top); - } - - private void retain() { - refs++; - } - - private void release() { - refs--; - if (refs <= 0 && texture != null) { - texture.close(); - } - } - } - - private record Bounds(float left, float bottom, float right, float top) { - } - - private record Glyph(float advance, float left, float bottom, float right, float top, float u0, float v0, float u1, - float v1, boolean visible) { - - private static Glyph from(float advance, - Bounds plane, - Bounds atlas, - boolean yOriginBottom, - int atlasWidth, - int atlasHeight, - float invW, - float invH) { - if (plane == null || atlas == null) { - return new Glyph(advance, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, false); - } - - float left = plane.left; - float right = plane.right; - float top = plane.top; - float bottom = plane.bottom; - - // The renderer expects plane Y values in screen-space (down-positive). - // MSDF atlases exported with bottom-origin store plane Y in up-positive space. - if (yOriginBottom) { - top = -top; - bottom = -bottom; - } - - float aLeft = atlas.left; - float aRight = atlas.right; - float aTop = atlas.top; - float aBottom = atlas.bottom; - - if (yOriginBottom) { - aTop = atlasHeight - aTop; - aBottom = atlasHeight - aBottom; - } - - float u0 = aLeft * invW; - float u1 = aRight * invW; - float v0 = aTop * invH; - float v1 = aBottom * invH; - - return new Glyph(advance, left, bottom, right, top, u0, v0, u1, v1, true); - } - } } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/msdf/MsdfGlyphDrawer.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/msdf/MsdfGlyphDrawer.java new file mode 100644 index 0000000..3d77ffb --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/msdf/MsdfGlyphDrawer.java @@ -0,0 +1,179 @@ +/* + * 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.text.msdf; + +import dev.loki.lovisual.render.engine.color.RenderColor; +import dev.loki.lovisual.render.engine.text.font.Font; +import dev.loki.lovisual.render.engine.text.glyph.GlyphFont; +import dev.loki.lovisual.render.engine.text.msdf.atlas.MsdfAtlas; +import dev.loki.lovisual.render.engine.text.msdf.atlas.MsdfGlyph; +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; +import org.jetbrains.annotations.Nullable; + +/** + * Итерация глифов MSDF-атласа и генерация квадов: сплошной цвет, + * линейный градиент (лево/право), квад-градиент (4 угла) и emitGlyphs. + */ +final class MsdfGlyphDrawer { + private MsdfGlyphDrawer() { + } + + static double render(MsdfAtlas atlas, float pixelScale, MeshBuilder mesh, + String string, double x, double y, RenderColor color, double scale) { + double baseline = y + atlas.ascender * pixelScale * scale; + int length = string.length(); + mesh.ensureCapacity(length * 4, length * 6); + + for (int i = 0; i < length; ) { + int cp = string.codePointAt(i); + MsdfGlyph g = glyph(atlas, cp); + if (g == null) { + i += Character.charCount(cp); + continue; + } + if (g.visible()) { + emit(mesh, g, quad(g, baseline, x, pixelScale * scale), color); + } + x += g.advance() * pixelScale * scale; + i += Character.charCount(cp); + } + return x; + } + + static double emitGlyphs(MsdfAtlas atlas, float pixelScale, + String string, double x, double y, double scale, GlyphFont.GlyphConsumer consumer) { + double baseline = y + atlas.ascender * pixelScale * scale; + int length = string.length(); + for (int i = 0; i < length; ) { + int cp = string.codePointAt(i); + MsdfGlyph g = glyph(atlas, cp); + if (g == null) { + i += Character.charCount(cp); + continue; + } + if (g.visible()) { + Quad q = quad(g, baseline, x, pixelScale * scale); + consumer.accept(q.x0(), q.y0(), q.x1(), q.y1(), g.u0(), g.v0(), g.u1(), g.v1()); + } + x += g.advance() * pixelScale * scale; + i += Character.charCount(cp); + } + return x; + } + + static double renderGradient(MsdfAtlas atlas, float pixelScale, MeshBuilder mesh, + String string, double x, double y, double scale, Font.GlyphGradient gradient) { + // Линейный градиент резолвит цвета даже у невидимых глифов (позиция пера), 1:1 со старым кодом. + return drawCorners(atlas, pixelScale, mesh, string, x, y, scale, true, (glyphIndex, cp, penX, q, out) -> { + gradient.colors(glyphIndex, cp, penX, out); + int left = out[0]; + int right = out[1]; + out[0] = left; + out[1] = left; + out[2] = right; + out[3] = right; + }); + } + + static double renderQuadGradient(MsdfAtlas atlas, float pixelScale, MeshBuilder mesh, + String string, double x, double y, double scale, Font.GlyphQuadGradient gradient) { + return drawCorners(atlas, pixelScale, mesh, string, x, y, scale, false, + (glyphIndex, cp, penX, q, out) -> gradient.colors(glyphIndex, cp, q.x0(), q.y0(), q.x1(), q.y1(), out)); + } + + private interface CornerResolver { + void resolve(int glyphIndex, int cp, double penX, Quad q, int[] out); + } + + private static double drawCorners(MsdfAtlas atlas, float pixelScale, MeshBuilder mesh, + String string, double x, double y, double scale, + boolean resolveInvisible, CornerResolver resolver) { + double baseline = y + atlas.ascender * pixelScale * scale; + int length = string.length(); + mesh.ensureCapacity(length * 4, length * 6); + + int[] colors = new int[4]; + int glyphIndex = 0; + for (int i = 0; i < length; ) { + int cp = string.codePointAt(i); + MsdfGlyph g = glyph(atlas, cp); + if (g == null) { + i += Character.charCount(cp); + glyphIndex++; + continue; + } + + Quad q = quad(g, baseline, x, pixelScale * scale); + if (g.visible()) { + resolver.resolve(glyphIndex, cp, x, q, colors); + emit(mesh, g, q, colors[0], colors[1], colors[2], colors[3]); + } else if (resolveInvisible) { + resolver.resolve(glyphIndex, cp, x, q, colors); + } + + x += g.advance() * pixelScale * scale; + i += Character.charCount(cp); + glyphIndex++; + } + return x; + } + + private static @Nullable MsdfGlyph glyph(MsdfAtlas atlas, int codePoint) { + MsdfGlyph g = atlas.glyphs.get(codePoint); + return g != null ? g : atlas.fallback; + } + + private record Quad(double x0, double y0, double x1, double y1) { + } + + private static Quad quad(MsdfGlyph g, double baseline, double x, double scalePx) { + return new Quad( + x + g.left() * scalePx, + baseline + g.top() * scalePx, + x + g.right() * scalePx, + baseline + g.bottom() * scalePx + ); + } + + private static void emit(MeshBuilder mesh, MsdfGlyph g, Quad q, RenderColor color) { + int i1 = mesh.vec2(q.x0(), q.y0()).raw2(g.u0(), g.v0()).color(color.r, color.g, color.b, color.a).next(); + int i2 = mesh.vec2(q.x0(), q.y1()).raw2(g.u0(), g.v1()).color(color.r, color.g, color.b, color.a).next(); + int i3 = mesh.vec2(q.x1(), q.y1()).raw2(g.u1(), g.v1()).color(color.r, color.g, color.b, color.a).next(); + int i4 = mesh.vec2(q.x1(), q.y0()).raw2(g.u1(), g.v0()).color(color.r, color.g, color.b, color.a).next(); + mesh.quad(i1, i2, i3, i4); + } + + private static void emit(MeshBuilder mesh, MsdfGlyph g, Quad q, + int topLeft, int bottomLeft, int bottomRight, int topRight) { + int i1 = mesh.vec2(q.x0(), q.y0()).raw2(g.u0(), g.v0()) + .color(red(topLeft), green(topLeft), blue(topLeft), alpha(topLeft)).next(); + int i2 = mesh.vec2(q.x0(), q.y1()).raw2(g.u0(), g.v1()) + .color(red(bottomLeft), green(bottomLeft), blue(bottomLeft), alpha(bottomLeft)).next(); + int i3 = mesh.vec2(q.x1(), q.y1()).raw2(g.u1(), g.v1()) + .color(red(bottomRight), green(bottomRight), blue(bottomRight), alpha(bottomRight)).next(); + int i4 = mesh.vec2(q.x1(), q.y0()).raw2(g.u1(), g.v0()) + .color(red(topRight), green(topRight), blue(topRight), alpha(topRight)).next(); + mesh.quad(i1, i2, i3, i4); + } + + private static int alpha(int argb) { + return (argb >>> 24) & 0xFF; + } + + private static int red(int argb) { + return (argb >>> 16) & 0xFF; + } + + private static int green(int argb) { + return (argb >>> 8) & 0xFF; + } + + private static int blue(int argb) { + return argb & 0xFF; + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfAtlas.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfAtlas.java new file mode 100644 index 0000000..6219962 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfAtlas.java @@ -0,0 +1,68 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.render.engine.text.msdf.atlas; + +import dev.loki.lovisual.render.engine.texture.Texture; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import org.jetbrains.annotations.Nullable; + +/** + * Загруженный MSDF-атлас: текстура, метрики и таблица глифов. + * Счётчик ссылок: несколько размеров MsdfFont делят один атлас. + */ +public final class MsdfAtlas { + public final Texture texture; + public final float distanceRange; + public final int width; + public final int height; + public final float lineHeight; + public final float ascender; + public final Int2ObjectOpenHashMap<MsdfGlyph> glyphs; + public final @Nullable MsdfGlyph fallback; + private int refs; + + MsdfAtlas(Texture texture, + float distanceRange, + int width, + int height, + float lineHeight, + float ascender, + Int2ObjectOpenHashMap<MsdfGlyph> glyphs, + @Nullable MsdfGlyph fallback) { + this.texture = texture; + this.distanceRange = distanceRange; + this.width = width; + this.height = height; + this.lineHeight = lineHeight; + this.ascender = ascender; + this.glyphs = glyphs; + this.fallback = fallback; + } + + /** + * Загружает атлас для builtin-шрифта; null и причина — через {@link #lastError()}. + */ + public static @Nullable MsdfAtlas load(dev.loki.lovisual.render.engine.text.font.info.FontInfo info) { + return MsdfAtlasLoader.load(info); + } + + public static String lastError() { + return MsdfAtlasLoader.lastError(); + } + + public void retain() { + refs++; + } + + public void release() { + refs--; + if (refs <= 0 && texture != null) { + texture.close(); + } + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfAtlasLoader.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfAtlasLoader.java new file mode 100644 index 0000000..216677b --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfAtlasLoader.java @@ -0,0 +1,171 @@ +/* + * 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.text.msdf.atlas; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.mojang.blaze3d.GpuFormat; +import com.mojang.blaze3d.textures.FilterMode; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import net.minecraft.client.Minecraft; +import net.minecraft.resources.Identifier; +import net.minecraft.server.packs.resources.ResourceManager; +import org.lwjgl.system.MemoryUtil; +import dev.loki.lovisual.render.engine.texture.Texture; +import dev.loki.lovisual.render.engine.text.font.FontUtils; +import dev.loki.lovisual.render.engine.text.font.info.FontInfo; +import dev.loki.lovisual.render.engine.text.render.TextRenderSystem; +import org.jetbrains.annotations.Nullable; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +/** + * Загрузка MSDF-атласа (msdf-bmfont JSON + PNG) из ресурсов и заливка в GPU-текстуру. + */ +final class MsdfAtlasLoader { + private static volatile String lastError = ""; + + private MsdfAtlasLoader() { + } + + static String lastError() { + return lastError; + } + + private static void setLastError(String reason) { + lastError = reason != null ? reason : ""; + } + + static @Nullable MsdfAtlas load(FontInfo info) { + Minecraft mc = Minecraft.getInstance(); + if (mc == null) { + setLastError("mc null"); + return null; + } + ResourceManager rm = mc.getResourceManager(); + + Identifier jsonId = FontUtils.msdfResource(info, "json"); + Identifier pngId = FontUtils.msdfResource(info, "png"); + if (jsonId == null || pngId == null) { + setLastError("msdf ids missing"); + return null; + } + + JsonObject root; + try (InputStream in = rm.getResource(jsonId).orElseThrow().open()) { + String json = new String(in.readAllBytes(), StandardCharsets.UTF_8); + root = JsonParser.parseString(json).getAsJsonObject(); + } catch (Exception ignored) { + setLastError("msdf json missing/invalid: " + jsonId); + return null; + } + + BufferedImage image; + try (InputStream in = rm.getResource(pngId).orElseThrow().open()) { + image = ImageIO.read(in); + if (image == null) return null; + } catch (Exception ignored) { + setLastError("msdf png missing/invalid: " + pngId); + return null; + } + + JsonObject atlasObj = root.has("atlas") ? root.getAsJsonObject("atlas") : null; + float distanceRange = MsdfJson.getFloat(atlasObj, "distanceRange", 4f); + int atlasWidth = MsdfJson.getInt(atlasObj, "width", image.getWidth()); + int atlasHeight = MsdfJson.getInt(atlasObj, "height", image.getHeight()); + if (atlasWidth <= 0) atlasWidth = image.getWidth(); + if (atlasHeight <= 0) atlasHeight = image.getHeight(); + String yOrigin = MsdfJson.getString(atlasObj, "yOrigin", "bottom"); + boolean yOriginBottom = !"top".equalsIgnoreCase(yOrigin); + + JsonObject metricsObj = root.has("metrics") ? root.getAsJsonObject("metrics") : null; + float emSize = MsdfJson.getFloat(metricsObj, "emSize", 1f); + float lineHeight = MsdfJson.getFloat(metricsObj, "lineHeight", emSize); + float ascender = MsdfJson.getFloat(metricsObj, "ascender", lineHeight); + if (ascender < 0f) ascender = -ascender; + if (lineHeight <= 0f) lineHeight = 1f; + + Int2ObjectOpenHashMap<MsdfGlyph> glyphs = readGlyphs(root, atlasWidth, atlasHeight, yOriginBottom); + + MsdfGlyph fallback = glyphs.get(32); + if (fallback == null) fallback = glyphs.get('?'); + + Texture texture = uploadTexture(image); + if (texture == null) { + setLastError("msdf texture upload failed: " + pngId); + return null; + } + + if (glyphs.isEmpty()) { + texture.close(); + setLastError("msdf glyphs empty: " + jsonId); + return null; + } + + setLastError(""); + TextRenderSystem.glyphAtlases().registerPage("msdf:" + jsonId, texture, atlasWidth, atlasHeight, true); + TextRenderSystem.glyphAtlases().onDirtyRectUpload(glyphs.size(), atlasWidth * atlasHeight * 4); + return new MsdfAtlas(texture, distanceRange, atlasWidth, atlasHeight, lineHeight, ascender, glyphs, fallback); + } + + private static Int2ObjectOpenHashMap<MsdfGlyph> readGlyphs(JsonObject root, int atlasWidth, int atlasHeight, boolean yOriginBottom) { + Int2ObjectOpenHashMap<MsdfGlyph> glyphs = new Int2ObjectOpenHashMap<>(); + JsonArray glyphArray = root.has("glyphs") ? root.getAsJsonArray("glyphs") : null; + if (glyphArray == null) return glyphs; + + float invW = 1f / atlasWidth; + float invH = 1f / atlasHeight; + for (JsonElement el : glyphArray) { + if (!el.isJsonObject()) continue; + JsonObject g = el.getAsJsonObject(); + + int codePoint = MsdfJson.readUnicode(g.get("unicode")); + if (codePoint < 0) continue; + + float advance = MsdfJson.getFloat(g, "advance", 0f); + MsdfGlyph.Bounds plane = MsdfJson.readBounds(g.has("planeBounds") ? g.getAsJsonObject("planeBounds") : null); + MsdfGlyph.Bounds atlas = MsdfJson.readBounds(g.has("atlasBounds") ? g.getAsJsonObject("atlasBounds") : null); + + MsdfGlyph glyph = MsdfGlyph.from(advance, plane, atlas, yOriginBottom, atlasWidth, atlasHeight, invW, invH); + glyphs.put(codePoint, glyph); + } + return glyphs; + } + + 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); + + ByteBuffer buffer = MemoryUtil.memAlloc(w * h * 4); + 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 texture = new Texture(w, h, GpuFormat.RGBA8_UNORM, FilterMode.LINEAR, FilterMode.LINEAR); + texture.upload(buffer); + return texture; + } catch (Exception ignored) { + return null; + } finally { + MemoryUtil.memFree(buffer); + } + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfGlyph.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfGlyph.java new file mode 100644 index 0000000..051199d --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfGlyph.java @@ -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.render.engine.text.msdf.atlas; + +/** + * Глиф MSDF-атласа: метрики в экранных пикселях + UV-координаты. + */ +public record MsdfGlyph(float advance, float left, float bottom, float right, float top, + float u0, float v0, float u1, float v1, boolean visible) { + + /** + * Границы прямоугольника из msdf-атласа (формат msdf-bmfont). + */ + public record Bounds(float left, float bottom, float right, float top) { + } + + public static MsdfGlyph from(float advance, + Bounds plane, + Bounds atlas, + boolean yOriginBottom, + int atlasWidth, + int atlasHeight, + float invW, + float invH) { + if (plane == null || atlas == null) { + return new MsdfGlyph(advance, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, false); + } + + float left = plane.left(); + float right = plane.right(); + float top = plane.top(); + float bottom = plane.bottom(); + + // The renderer expects plane Y values in screen-space (down-positive). + // MSDF atlases exported with bottom-origin store plane Y in up-positive space. + if (yOriginBottom) { + top = -top; + bottom = -bottom; + } + + float aLeft = atlas.left(); + float aRight = atlas.right(); + float aTop = atlas.top(); + float aBottom = atlas.bottom(); + + if (yOriginBottom) { + aTop = atlasHeight - aTop; + aBottom = atlasHeight - aBottom; + } + + float u0 = aLeft * invW; + float u1 = aRight * invW; + float v0 = aTop * invH; + float v1 = aBottom * invH; + + return new MsdfGlyph(advance, left, bottom, right, top, u0, v0, u1, v1, true); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfJson.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfJson.java new file mode 100644 index 0000000..3b02dd6 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/msdf/atlas/MsdfJson.java @@ -0,0 +1,78 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.render.engine.text.msdf.atlas; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import org.jetbrains.annotations.Nullable; + +/** + * Толерантное чтение полей msdf-bmfont JSON: любое отсутствие/несоответствие + * типа отдаёт fallback вместо исключения. + */ +final class MsdfJson { + private MsdfJson() { + } + + static float getFloat(JsonObject obj, String key, float fallback) { + if (obj == null || key == null || !obj.has(key)) return fallback; + try { + return obj.get(key).getAsFloat(); + } catch (Exception ignored) { + return fallback; + } + } + + static int getInt(JsonObject obj, String key, int fallback) { + if (obj == null || key == null || !obj.has(key)) return fallback; + try { + return obj.get(key).getAsInt(); + } catch (Exception ignored) { + return fallback; + } + } + + static String getString(JsonObject obj, String key, String fallback) { + if (obj == null || key == null || !obj.has(key)) return fallback; + try { + return obj.get(key).getAsString(); + } catch (Exception ignored) { + return fallback; + } + } + + static int readUnicode(JsonElement element) { + if (element == null) return -1; + try { + if (element.isJsonPrimitive()) { + var prim = element.getAsJsonPrimitive(); + if (prim.isNumber()) return prim.getAsInt(); + if (prim.isString()) { + String str = prim.getAsString(); + if (str.isEmpty()) return -1; + if (str.length() == 1) return str.codePointAt(0); + if (str.startsWith("0x") || str.startsWith("0X")) { + return Integer.parseInt(str.substring(2), 16); + } + return Integer.parseInt(str); + } + } + } catch (Exception ignored) { + } + return -1; + } + + static @Nullable MsdfGlyph.Bounds readBounds(JsonObject obj) { + if (obj == null) return null; + float left = getFloat(obj, "left", 0f); + float right = getFloat(obj, "right", 0f); + float top = getFloat(obj, "top", 0f); + float bottom = getFloat(obj, "bottom", 0f); + return new MsdfGlyph.Bounds(left, bottom, right, top); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/HandChamsUniforms.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/HandChamsUniforms.java index 3fa72e9..710ed3a 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/HandChamsUniforms.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/hand/HandChamsUniforms.java @@ -5,7 +5,7 @@ import com.mojang.blaze3d.buffers.Std140Builder; import com.mojang.blaze3d.buffers.Std140SizeCalculator; import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; import dev.loki.lovisual.render.engine.rhi.uniform.LoVisualUniformAllocator; -import dev.loki.lovisual.features.module.modules.visuals.scene.Chams; +import dev.loki.lovisual.features.module.modules.visuals.scene.visual.Chams; import java.nio.ByteBuffer; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/SkyShaderNebulaUniforms.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/postprocess/SkyShaderNebulaUniforms.java similarity index 98% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/SkyShaderNebulaUniforms.java rename to combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/postprocess/SkyShaderNebulaUniforms.java index a35d86c..c2964f2 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/SkyShaderNebulaUniforms.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/postprocess/SkyShaderNebulaUniforms.java @@ -5,7 +5,7 @@ * Licensed under the GNU General Public License v3.0. */ -package dev.loki.lovisual.render.engine.uniform.impl.sky; +package dev.loki.lovisual.render.engine.uniform.impl.sky.postprocess; import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/SkyShaderStarryUniforms.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/postprocess/SkyShaderStarryUniforms.java similarity index 98% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/SkyShaderStarryUniforms.java rename to combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/postprocess/SkyShaderStarryUniforms.java index 01172ff..87ce38f 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/SkyShaderStarryUniforms.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/postprocess/SkyShaderStarryUniforms.java @@ -5,7 +5,7 @@ * Licensed under the GNU General Public License v3.0. */ -package dev.loki.lovisual.render.engine.uniform.impl.sky; +package dev.loki.lovisual.render.engine.uniform.impl.sky.postprocess; import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/SkyShaderUniforms.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/postprocess/SkyShaderUniforms.java similarity index 97% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/SkyShaderUniforms.java rename to combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/postprocess/SkyShaderUniforms.java index d9c8c11..915c106 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/SkyShaderUniforms.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/postprocess/SkyShaderUniforms.java @@ -5,7 +5,7 @@ * Licensed under the GNU General Public License v3.0. */ -package dev.loki.lovisual.render.engine.uniform.impl.sky; +package dev.loki.lovisual.render.engine.uniform.impl.sky.postprocess; import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/ShaderSkyUniforms.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/skybox/ShaderSkyUniforms.java similarity index 97% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/ShaderSkyUniforms.java rename to combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/skybox/ShaderSkyUniforms.java index cd03b30..d2b5dc6 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/ShaderSkyUniforms.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/skybox/ShaderSkyUniforms.java @@ -1,11 +1,11 @@ -package dev.loki.lovisual.render.engine.uniform.impl.sky; +package dev.loki.lovisual.render.engine.uniform.impl.sky.skybox; import com.mojang.blaze3d.buffers.GpuBufferSlice; import com.mojang.blaze3d.buffers.Std140Builder; import com.mojang.blaze3d.buffers.Std140SizeCalculator; import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; import dev.loki.lovisual.render.engine.rhi.uniform.LoVisualUniformAllocator; -import dev.loki.lovisual.features.module.modules.visuals.weather.ShaderSky; +import dev.loki.lovisual.features.module.modules.visuals.weather.sky.ShaderSky; import java.nio.ByteBuffer; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/SkyboxShaderUniforms.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/skybox/SkyboxShaderUniforms.java similarity index 99% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/SkyboxShaderUniforms.java rename to combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/skybox/SkyboxShaderUniforms.java index 8129469..6d3b517 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/SkyboxShaderUniforms.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/skybox/SkyboxShaderUniforms.java @@ -5,7 +5,7 @@ * Licensed under the GNU General Public License v3.0. */ -package dev.loki.lovisual.render.engine.uniform.impl.sky; +package dev.loki.lovisual.render.engine.uniform.impl.sky.skybox; import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/KillEffectBeamUniforms.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/KillEffectBeamUniforms.java index 8dc1684..eb64dcb 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/KillEffectBeamUniforms.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/world/KillEffectBeamUniforms.java @@ -13,7 +13,7 @@ import com.mojang.blaze3d.buffers.GpuBufferSlice; import com.mojang.blaze3d.buffers.Std140Builder; import com.mojang.blaze3d.buffers.Std140SizeCalculator; import dev.loki.lovisual.render.engine.rhi.uniform.LoVisualUniformAllocator; -import dev.loki.lovisual.features.module.modules.visuals.effects.KillEffect; +import dev.loki.lovisual.features.module.modules.visuals.effects.hits.KillEffect; /** Uniform buffer for the KillEffect "Portal"/"Tornado" world-space beam shaders. */ public enum KillEffectBeamUniforms { diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/helpers/material/SodiumSurfaceFlagContext.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/helpers/material/SodiumSurfaceFlagContext.java index fcdbefc..11a243e 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/helpers/material/SodiumSurfaceFlagContext.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/helpers/material/SodiumSurfaceFlagContext.java @@ -16,7 +16,7 @@ import net.minecraft.world.level.block.VegetationBlock; import net.minecraft.world.level.block.VineBlock; import net.minecraft.world.level.block.state.BlockState; import dev.loki.lovisual.features.module.modules.visuals.visibility.NoRender; -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.util.logging.DebugLog; import java.util.ArrayDeque; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/iris/compat/IrisCompatibilityGuards.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/iris/compat/IrisCompatibilityGuards.java index 8495562..d497854 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/iris/compat/IrisCompatibilityGuards.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/iris/compat/IrisCompatibilityGuards.java @@ -10,7 +10,7 @@ package dev.loki.lovisual.render.iris.compat; import dev.loki.lovisual.features.module.lifecycle.Modules; import dev.loki.lovisual.features.module.modules.visuals.fx.MotionBlur; import dev.loki.lovisual.Lifecycle; -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.compat.IrisCompatibilityFeature; import dev.loki.lovisual.render.iris.runtime.IrisRuntime; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/sky/CustomSkyboxRenderer.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/sky/CustomSkyboxRenderer.java index d590925..2b04ce4 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/sky/CustomSkyboxRenderer.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/sky/CustomSkyboxRenderer.java @@ -30,8 +30,8 @@ import org.joml.Vector3f; import dev.loki.lovisual.features.module.lifecycle.Modules; import dev.loki.lovisual.features.module.modules.visuals.camera.Freecam; import dev.loki.lovisual.features.module.modules.visuals.visibility.NoRender; -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.theme.Theme; import dev.loki.lovisual.mixins.accessors.renderer.GameRendererAccessor; import dev.loki.lovisual.mixins.accessors.entity.LocalPlayerAccessor; @@ -42,9 +42,9 @@ 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.MeshBuilder; -import dev.loki.lovisual.render.engine.uniform.impl.sky.ShaderSkyUniforms; +import dev.loki.lovisual.render.engine.uniform.impl.sky.skybox.ShaderSkyUniforms; import dev.loki.lovisual.render.engine.uniform.impl.sky.sun.SkySunUniforms; -import dev.loki.lovisual.render.engine.uniform.impl.sky.SkyboxShaderUniforms; +import dev.loki.lovisual.render.engine.uniform.impl.sky.skybox.SkyboxShaderUniforms; public enum CustomSkyboxRenderer { ; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/EquipProgressComposer.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/EquipProgressComposer.java index 0a9d6ee..c145386 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/EquipProgressComposer.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/EquipProgressComposer.java @@ -7,7 +7,7 @@ package dev.loki.lovisual.util.animation; import dev.loki.lovisual.features.module.modules.visuals.camera.ViewModel; -import dev.loki.lovisual.features.module.modules.visuals.items.HoldMyItems; +import dev.loki.lovisual.features.module.modules.visuals.items.held.HoldMyItems; /** * Единая цепочка обработки equip-прогресса руки: сначала кривая HoldMyItems diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/HandDynamics.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/HandDynamics.java index f3b9c2c..870dd13 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/HandDynamics.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/HandDynamics.java @@ -16,7 +16,7 @@ import net.minecraft.client.player.AbstractClientPlayer; import net.minecraft.world.InteractionHand; import net.minecraft.world.entity.HumanoidArm; import net.minecraft.world.item.ItemStack; -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.render.helpers.util.TickDelta; /** diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/click/AttackPressing.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/click/AttackPressing.java index 68e23ee..a409872 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/click/AttackPressing.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/click/AttackPressing.java @@ -15,7 +15,6 @@ import net.minecraft.world.item.Items; import dev.loki.lovisual.events.meta.EventHandler; import dev.loki.lovisual.events.impl.network.PacketEvent; import dev.loki.lovisual.util.combat.rotation.LethalStrikeCalculator; -import dev.loki.lovisual.util.player.inventory.core.InventorySwap; /** * Shared modern attack gate logic. @@ -81,10 +80,6 @@ public final class AttackPressing { @EventHandler private void onPacketSend(PacketEvent.Send event) { - if (InventorySwap.INSTANCE.isInternalSwap()) { - return; - } - if (event.getPacket() instanceof ServerboundSwingPacket || event.getPacket() instanceof ServerboundSetCarriedItemPacket) { recalculate(); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/attack/AttackUtil.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/attack/AttackUtil.java index c8d5b56..9b90227 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/attack/AttackUtil.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/attack/AttackUtil.java @@ -21,8 +21,8 @@ import dev.loki.lovisual.features.gui.hud.draggable.impl.targethud.TargetHud; import dev.loki.lovisual.features.module.lifecycle.Modules; import dev.loki.lovisual.features.module.modules.combat.pvp.RubberHand; import dev.loki.lovisual.features.module.modules.misc.sound.HitSounds; -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.entity.LocalPlayerAccessor; import dev.loki.lovisual.mixins.accessors.misc.MultiPlayerGameModeAccessor; import dev.loki.lovisual.util.item.food.FoodUtil; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/inventory/core/InventorySwap.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/inventory/core/InventorySwap.java deleted file mode 100644 index 7000e2c..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/inventory/core/InventorySwap.java +++ /dev/null @@ -1,1038 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.util.player.inventory.core; - -import net.minecraft.client.Minecraft; -import net.minecraft.client.multiplayer.MultiPlayerGameMode; -import net.minecraft.client.player.LocalPlayer; -import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.network.protocol.game.*; -import net.minecraft.world.Container; -import net.minecraft.world.InteractionHand; -import net.minecraft.world.InteractionResult; -import net.minecraft.world.entity.player.Input; -import net.minecraft.world.inventory.AbstractContainerMenu; -import net.minecraft.world.inventory.ContainerInput; -import net.minecraft.world.inventory.Slot; -import net.minecraft.world.item.ItemStack; -import dev.loki.lovisual.events.meta.EventHandler; -import dev.loki.lovisual.events.impl.render.GameTickEvent; -import dev.loki.lovisual.events.impl.movement.MovementInputEvent; -import dev.loki.lovisual.events.impl.network.PacketEvent; -import dev.loki.lovisual.events.impl.input.SprintControlEvent; -import dev.loki.lovisual.mixins.accessors.misc.MultiPlayerGameModeAccessor; -import dev.loki.lovisual.mixins.accessors.inventory.PlayerInventoryAccessor; -import dev.loki.lovisual.mixins.accessors.inventory.SlotAccessor; - -import java.util.ArrayDeque; -import java.util.Deque; -import java.util.Set; -import java.util.UUID; -import java.util.function.BooleanSupplier; -import java.util.function.Predicate; -import dev.loki.lovisual.util.player.inventory.core.InventorySwapMode; -import dev.loki.lovisual.util.player.inventory.core.InventorySwapPolicy; -import dev.loki.lovisual.util.player.inventory.core.InventorySwapRequest; -import dev.loki.lovisual.util.player.inventory.support.InventoryActionKind; -import dev.loki.lovisual.util.player.inventory.support.InventorySearchScope; -import dev.loki.lovisual.util.player.inventory.support.InventorySwapVisibility; -import dev.loki.lovisual.util.player.inventory.support.SlotResult; - -public final class InventorySwap { - public static final InventorySwap INSTANCE = new InventorySwap(); - - private static final int OFFHAND_SCREEN_SLOT = 45; - private static final int OFFHAND_SWAP_BUTTON = 40; - - private final Deque<QueuedAction> strictQueue = new ArrayDeque<>(); - private int lastServerSelectedSlot = -1; - private int savedSelectedSlot = -1; - private int internalSwapDepth; - private HotbarLease hotbarLease; - private InventorySwapPolicy defaultPolicy = InventorySwapPolicy.NONE; - private InventorySearchScope defaultScope = InventorySearchScope.FULL; - private InventorySwapVisibility defaultVisibility = InventorySwapVisibility.SILENT; - private boolean restoreByDefault = true; - private boolean preferHotbar = true; - private int legitWaitTicks = 2; - private int strictInventoryWaitTicks = 1; - private int strictMovementLockTicks = 2; - private int packetTick; - private int lastUseItemTick = Integer.MIN_VALUE; - private int lastEntityActionTick = Integer.MIN_VALUE; - private int lastBlockActionTick = Integer.MIN_VALUE; - private boolean lastInputMoving; - private int movementLockTicks; - private boolean sendingLockedInput; - - private InventorySwap() { - } - - public static int mapInventoryToScreenSlot(int inventorySlot) { - Minecraft mc = Minecraft.getInstance(); - LocalPlayer player = mc.player; - AbstractContainerMenu handler = player != null ? player.containerMenu : null; - Container inventory = player != null ? player.getInventory() : null; - return mapInventoryToScreenSlot(handler, inventory, inventorySlot); - } - - public static int mapInventoryToScreenSlot(AbstractContainerMenu handler, Container playerInventory, int inventorySlot) { - if (inventorySlot < 0 || inventorySlot >= 41) return -1; - if (handler != null && playerInventory != null) { - for (Slot slot : handler.slots) { - if (slot == null || slot.container != playerInventory) continue; - if (((SlotAccessor) slot).lovisual$getIndex() == inventorySlot) { - return slot.index; - } - } - } - return legacyPlayerScreenSlot(inventorySlot); - } - - public static int mapHotbarToScreenSlot(int hotbarSlot) { - if (!isHotbarSlot(hotbarSlot)) return -1; - return mapInventoryToScreenSlot(hotbarSlot); - } - - public static int playerInventoryStart(AbstractContainerMenu handler) { - Minecraft mc = Minecraft.getInstance(); - if (handler == null) return -1; - LocalPlayer player = mc.player; - Container inventory = player != null ? player.getInventory() : null; - if (handler != null && inventory != null) { - int min = Integer.MAX_VALUE; - for (Slot slot : handler.slots) { - if (slot == null || slot.container != inventory) continue; - int index = ((SlotAccessor) slot).lovisual$getIndex(); - if (index >= 9 && index < 36) { - min = Math.min(min, slot.index); - } - } - if (min != Integer.MAX_VALUE) return min; - } - return handler.slots.size() - 36; - } - - public static int hotbarStart(AbstractContainerMenu handler) { - Minecraft mc = Minecraft.getInstance(); - if (handler == null) return -1; - LocalPlayer player = mc.player; - Container inventory = player != null ? player.getInventory() : null; - if (handler != null && inventory != null) { - int min = Integer.MAX_VALUE; - for (Slot slot : handler.slots) { - if (slot == null || slot.container != inventory) continue; - int index = ((SlotAccessor) slot).lovisual$getIndex(); - if (index >= 0 && index < 9) { - min = Math.min(min, slot.index); - } - } - if (min != Integer.MAX_VALUE) return min; - } - return playerInventoryStart(handler) + 27; - } - - private static int legacyPlayerScreenSlot(int inventorySlot) { - if (inventorySlot == 40) return OFFHAND_SCREEN_SLOT; - return inventorySlot < 9 ? inventorySlot + 36 : inventorySlot; - } - - private static boolean isHotbarSlot(int slot) { - return slot >= 0 && slot <= 8; - } - - private static boolean isSwapButton(int button) { - return isHotbarSlot(button) || button == OFFHAND_SWAP_BUTTON; - } - - @EventHandler - private void onPacketSend(PacketEvent.Send event) { - Minecraft mc = Minecraft.getInstance(); - Object packet = event.getPacket(); - if (packet instanceof ServerboundSetCarriedItemPacket selectedSlot) { - lastServerSelectedSlot = selectedSlot.getSlot(); - } else if (packet instanceof ServerboundMovePlayerPacket || packet instanceof ServerboundClientTickEndPacket) { - packetTick++; - } else if (packet instanceof ServerboundPlayerInputPacket(Input input)) { - boolean moving = input.forward() || input.backward() || input.left() || input.right(); - if (isMovementLocked() && !sendingLockedInput && moving && mc.getConnection() != null) { - event.cancel(); - sendingLockedInput = true; - try { - mc.getConnection().send(new ServerboundPlayerInputPacket( - new Input(false, false, false, false, false, false, false) - )); - } finally { - sendingLockedInput = false; - } - lastInputMoving = false; - return; - } - lastInputMoving = moving; - } else if (packet instanceof ServerboundUseItemPacket) { - lastUseItemTick = packetTick; - } else if (packet instanceof ServerboundSwingPacket) { - // MultiActionsE checks animation while using item; the safety gate uses player.isUsingItem(). - } else if (packet instanceof ServerboundInteractPacket) { - lastEntityActionTick = packetTick; - } else if (packet instanceof ServerboundUseItemOnPacket) { - lastBlockActionTick = packetTick; - } else if (packet instanceof ServerboundPlayerActionPacket action - && (action.getAction() == ServerboundPlayerActionPacket.Action.START_DESTROY_BLOCK - || action.getAction() == ServerboundPlayerActionPacket.Action.STOP_DESTROY_BLOCK)) { - lastBlockActionTick = packetTick; - } else if (packet instanceof ServerboundContainerClickPacket || packet instanceof ServerboundContainerClosePacket) { - // Present for explicit packet classification. Grim C/D safety is evaluated before sending. - } - } - - @EventHandler - private void onGameTick(GameTickEvent event) { - tick(); - if (movementLockTicks > 0) { - movementLockTicks--; - } - - if (strictQueue.isEmpty()) { - return; - } - - requestStrictMovementLock(); - - QueuedAction next = strictQueue.peekFirst(); - if (next == null) return; - - if (next.waitTicks > 0) { - next.waitTicks--; - return; - } - - if (!next.isSafe()) { - next.waitTicks = 1; - return; - } - - strictQueue.removeFirst(); - runGuarded(next.action); - - if (strictQueue.isEmpty()) { - requestMovementLock(1); - } - } - - @EventHandler(priority = -10000) - private void onMovementInput(MovementInputEvent event) { - if (!isMovementLocked()) return; - - event.setForward(false); - event.setBackward(false); - event.setLeft(false); - event.setRight(false); - event.setJump(false); - event.setSneak(false); - event.setSprint(false); - lastInputMoving = false; - } - - @EventHandler(priority = -10000) - private void onSprintControl(SprintControlEvent event) { - if (isMovementLocked()) { - event.setSprint(false); - } - } - - public boolean isInternalSwap() { - return internalSwapDepth > 0; - } - - public boolean hasPendingStrictActions() { - return !strictQueue.isEmpty(); - } - - public void clearPendingStrictActions() { - strictQueue.clear(); - movementLockTicks = 0; - } - - public boolean canRunStrict(InventoryActionKind kind) { - return isSafeFor(kind, InventorySwapPolicy.GRIM_STRICT); - } - - public InventorySwapPolicy defaultPolicy() { - return defaultPolicy; - } - - public void setDefaultPolicy(InventorySwapPolicy defaultPolicy) { - this.defaultPolicy = defaultPolicy != null ? defaultPolicy : InventorySwapPolicy.NONE; - } - - public InventorySearchScope defaultScope() { - return defaultScope; - } - - public void setDefaultScope(InventorySearchScope defaultScope) { - this.defaultScope = defaultScope != null ? defaultScope : InventorySearchScope.FULL; - } - - public InventorySwapVisibility defaultVisibility() { - return defaultVisibility; - } - - public void setDefaultVisibility(InventorySwapVisibility defaultVisibility) { - this.defaultVisibility = defaultVisibility != null ? defaultVisibility : InventorySwapVisibility.SILENT; - } - - public boolean restoreByDefault() { - return restoreByDefault; - } - - public void setRestoreByDefault(boolean restoreByDefault) { - this.restoreByDefault = restoreByDefault; - } - - public boolean preferHotbar() { - return preferHotbar; - } - - public void setPreferHotbar(boolean preferHotbar) { - this.preferHotbar = preferHotbar; - } - - public int legitWaitTicks() { - return legitWaitTicks; - } - - public void setLegitWaitTicks(int legitWaitTicks) { - this.legitWaitTicks = Math.max(0, legitWaitTicks); - } - - public int strictInventoryWaitTicks() { - return strictInventoryWaitTicks; - } - - public void setStrictInventoryWaitTicks(int strictInventoryWaitTicks) { - this.strictInventoryWaitTicks = Math.max(0, strictInventoryWaitTicks); - } - - public int strictMovementLockTicks() { - return strictMovementLockTicks; - } - - public void setStrictMovementLockTicks(int strictMovementLockTicks) { - this.strictMovementLockTicks = Math.max(0, strictMovementLockTicks); - } - - public void resetDefaults() { - defaultPolicy = InventorySwapPolicy.NONE; - defaultScope = InventorySearchScope.FULL; - defaultVisibility = InventorySwapVisibility.SILENT; - restoreByDefault = true; - preferHotbar = true; - legitWaitTicks = 2; - strictInventoryWaitTicks = 1; - strictMovementLockTicks = 2; - } - - public int serverSelectedSlot() { - HotbarLease active = activeLease(); - if (active != null) return active.enforcedSlot; - if (lastServerSelectedSlot >= 0 && lastServerSelectedSlot <= 8) return lastServerSelectedSlot; - return clientSelectedSlot(); - } - - public int effectiveSelectedSlot() { - HotbarLease active = activeLease(); - return active != null ? active.enforcedSlot : clientSelectedSlot(); - } - - public int clientSelectedSlot() { - Minecraft mc = Minecraft.getInstance(); - LocalPlayer player = mc.player; - if (player == null) return 0; - return ((PlayerInventoryAccessor) player.getInventory()).lovisual$getSelectedSlot(); - } - - public boolean isHotbarLeased() { - return activeLease() != null; - } - - public boolean isHotbarLeasedBy(Object owner) { - HotbarLease active = activeLease(); - return active != null && active.owner == owner; - } - - public boolean leaseHotbar(Object owner, int slot, int ticksUntilReset) { - Minecraft mc = Minecraft.getInstance(); - LocalPlayer player = mc.player; - if (player == null || !isHotbarSlot(slot)) return false; - - int resetAfterAge = player.tickCount + Math.max(0, ticksUntilReset) + 1; - hotbarLease = new HotbarLease(player.getUUID(), owner, slot, clientSelectedSlot(), resetAfterAge); - if (!sendSelectedSlotIfNeeded(slot)) { - hotbarLease = null; - return false; - } - return true; - } - - public void releaseHotbar(Object owner) { - HotbarLease active = activeLease(); - if (active != null && active.owner == owner) { - clearLease(active, true); - } - } - - public void tick() { - activeLease(); - } - - public void saveSelectedSlot() { - savedSelectedSlot = clientSelectedSlot(); - } - - public void restoreSavedSelectedSlot() { - if (isHotbarSlot(savedSelectedSlot)) { - selectHotbar(savedSelectedSlot); - } - savedSelectedSlot = -1; - } - - public boolean selectHotbar(int slot) { - Minecraft mc = Minecraft.getInstance(); - LocalPlayer player = mc.player; - if (player == null || mc.getConnection() == null || !isHotbarSlot(slot)) return false; - - int current = clientSelectedSlot(); - if (current != slot) { - ((PlayerInventoryAccessor) player.getInventory()).lovisual$setSelectedSlot(slot); - } - - syncInteractionManagerSlot(slot); - if (lastServerSelectedSlot != slot) { - mc.getConnection().send(new ServerboundSetCarriedItemPacket(slot)); - lastServerSelectedSlot = slot; - } - return true; - } - - public boolean sendSelectedSlotIfNeeded(int slot) { - Minecraft mc = Minecraft.getInstance(); - if (mc.player == null || mc.getConnection() == null || !isHotbarSlot(slot)) return false; - if (lastServerSelectedSlot != slot) { - mc.getConnection().send(new ServerboundSetCarriedItemPacket(slot)); - lastServerSelectedSlot = slot; - } - syncInteractionManagerSlot(slot); - return true; - } - - public boolean clickSwap(int containerSlot) { - return clickSwap(containerSlot, clientSelectedSlot()); - } - - public boolean clickSwap(int containerSlot, int hotbarButton) { - Minecraft mc = Minecraft.getInstance(); - LocalPlayer player = mc.player; - MultiPlayerGameMode interaction = mc.gameMode; - AbstractContainerMenu handler = player != null ? player.containerMenu : null; - if (player == null || interaction == null || handler == null) return false; - if (!isSwapButton(hotbarButton) || containerSlot < 0) return false; - - beginInternalSwap(); - try { - interaction.handleContainerInput(handler.containerId, containerSlot, hotbarButton, ContainerInput.SWAP, player); - return true; - } finally { - endInternalSwap(); - } - } - - public boolean pickupSwap(int slotA, int slotB) { - Minecraft mc = Minecraft.getInstance(); - LocalPlayer player = mc.player; - MultiPlayerGameMode interaction = mc.gameMode; - AbstractContainerMenu handler = player != null ? player.containerMenu : null; - if (player == null || interaction == null || handler == null) return false; - if (slotA < 0 || slotB < 0 || slotA == slotB) return false; - - beginInternalSwap(); - try { - interaction.handleContainerInput(handler.containerId, slotA, 0, ContainerInput.PICKUP, player); - interaction.handleContainerInput(handler.containerId, slotB, 0, ContainerInput.PICKUP, player); - interaction.handleContainerInput(handler.containerId, slotA, 0, ContainerInput.PICKUP, player); - return true; - } finally { - endInternalSwap(); - } - } - - public boolean swapScreenSlots(int slotA, int slotB) { - return command(InventoryActionKind.INVENTORY_CLICK, () -> pickupSwap(slotA, slotB)); - } - - public boolean swapInventoryToHotbar(int inventorySlot, int hotbarSlot) { - if (!isHotbarSlot(hotbarSlot) || inventorySlot < 0 || inventorySlot >= 36) return false; - if (inventorySlot == hotbarSlot) return selectHotbar(hotbarSlot); - - int sourceScreenSlot = mapInventoryToScreenSlot(inventorySlot); - int targetScreenSlot = mapHotbarToScreenSlot(hotbarSlot); - return command(InventoryActionKind.INVENTORY_CLICK, () -> { - pickupSwap(sourceScreenSlot, targetScreenSlot); - selectHotbar(hotbarSlot); - }); - } - - public boolean swapInventoryToOffhand(int inventorySlot) { - return swapInventoryToOffhand(inventorySlot, null); - } - - public boolean swapInventoryToOffhand(int inventorySlot, Runnable afterSwap) { - if (inventorySlot < 0 || inventorySlot >= 36) return false; - int sourceScreenSlot = mapInventoryToScreenSlot(inventorySlot); - return command(InventoryActionKind.INVENTORY_CLICK, () -> { - clickSwap(sourceScreenSlot, OFFHAND_SWAP_BUTTON); - if (afterSwap != null) { - afterSwap.run(); - } - }); - } - - public boolean dropStack(Slot slot) { - Minecraft mc = Minecraft.getInstance(); - LocalPlayer player = mc.player; - MultiPlayerGameMode interaction = mc.gameMode; - AbstractContainerMenu handler = player != null ? player.containerMenu : null; - if (player == null || interaction == null || handler == null || slot == null) return false; - int slotId = slot.index; - if (slotId < 0 || slotId >= handler.slots.size()) return false; - - return command(InventoryActionKind.INVENTORY_CLICK, () -> { - beginInternalSwap(); - try { - interaction.handleContainerInput(handler.containerId, slotId, 1, ContainerInput.THROW, player); - } finally { - endInternalSwap(); - } - }); - } - - public int quickMoveMatchingFromContainer(Set<String> itemIds) { - Minecraft mc = Minecraft.getInstance(); - LocalPlayer player = mc.player; - MultiPlayerGameMode interaction = mc.gameMode; - AbstractContainerMenu handler = player != null ? player.containerMenu : null; - if (player == null || interaction == null || handler == null || itemIds == null || itemIds.isEmpty()) return 0; - - int moved = 0; - int playerStart = playerInventoryStart(handler); - for (int slotId = 0; slotId < playerStart; slotId++) { - Slot slot = handler.slots.get(slotId); - if (slot == null || !slot.hasItem()) continue; - - String itemId = BuiltInRegistries.ITEM.getKey(slot.getItem().getItem()).toString().toLowerCase(); - if (!itemIds.contains(itemId)) continue; - - int finalSlotId = slotId; - if (command(InventoryActionKind.INVENTORY_CLICK, () -> { - beginInternalSwap(); - try { - interaction.handleContainerInput(handler.containerId, finalSlotId, 0, ContainerInput.QUICK_MOVE, player); - } finally { - endInternalSwap(); - } - })) { - moved++; - } - } - return moved; - } - - public boolean withSwap(SlotResult result, InventorySwapMode mode, Runnable action) { - if (result == null || !result.found()) return false; - return withSwap(result.slot(), mode, action); - } - - public boolean withSwap(SlotResult result, InventorySwapMode mode, InventorySwapPolicy policy, - InventoryActionKind kind, Runnable action) { - if (result == null || !result.found()) return false; - return withSwap(result.slot(), mode, policy, kind, action); - } - - public boolean withSwap(int slot, InventorySwapMode mode, Runnable action) { - if (action == null || mode == null) return false; - - beginInternalSwap(); - try { - return switch (mode) { - case NORMAL -> runNormal(slot, action); - case SILENT -> runSilent(slot, action); - case INVENTORY_NORMAL -> runInventoryNormal(slot, action); - case INVENTORY_SILENT -> runInventorySilent(slot, action); - case SILENT_FULL -> - withSwap(slot, isHotbarSlot(slot) ? InventorySwapMode.SILENT : InventorySwapMode.INVENTORY_SILENT, action); - case NORMAL_FULL -> - withSwap(slot, isHotbarSlot(slot) ? InventorySwapMode.NORMAL : InventorySwapMode.INVENTORY_NORMAL, action); - }; - } finally { - endInternalSwap(); - } - } - - public boolean withSwap(int slot, InventorySwapMode mode, InventorySwapPolicy policy, - InventoryActionKind kind, Runnable action) { - if (policy == null || policy == InventorySwapPolicy.NONE) { - return withSwap(slot, mode, action); - } - if (action == null || mode == null) return false; - - Runnable wrapped = () -> withSwap(slot, mode, action); - BooleanSupplier safety = () -> isSafeForSwap(slot, mode, kind, policy); - if (safety.getAsBoolean()) { - runGuarded(wrapped); - return true; - } - - strictQueue.addLast(new QueuedAction(kind, policy, wrapped, initialWaitTicks(policy, kind), safety)); - requestStrictMovementLock(); - return true; - } - - public SlotResult findSlotForMode(Predicate<ItemStack> predicate, InventorySwapMode mode) { - if (predicate == null || mode == null) return SlotResult.empty(); - return switch (mode) { - case NORMAL, SILENT -> findHotbar(predicate); - case INVENTORY_NORMAL, INVENTORY_SILENT -> findInventory(predicate); - case SILENT_FULL, NORMAL_FULL -> { - SlotResult hotbar = findHotbar(predicate); - yield hotbar.found() ? hotbar : findInventory(predicate); - } - }; - } - - public boolean withFoundSlot(Predicate<ItemStack> predicate, InventorySwapMode mode, Runnable action) { - SlotResult result = findSlotForMode(predicate, mode); - return result.found() && withSwap(result, mode, action); - } - - public boolean withFoundSlot(Predicate<ItemStack> predicate, InventorySwapMode mode, InventorySwapPolicy policy, - InventoryActionKind kind, Runnable action) { - SlotResult result = findSlotForMode(predicate, mode); - return result.found() && withSwap(result, mode, policy, kind, action); - } - - public boolean execute(InventorySwapRequest request) { - if (request == null || request.predicate() == null || request.action() == null) return false; - - InventorySearchScope scope = resolveScope(request); - InventorySwapMode mode = resolveMode(request, scope); - SlotResult result = findSlotForRequest(request, mode, scope); - if (!result.found()) return false; - - InventorySwapPolicy policy = request.policy(defaultPolicy); - return withSwap(result, mode, policy, request.actionKind(), request.action()); - } - - public boolean command(InventorySwapRequest request) { - return execute(request); - } - - public boolean command(InventoryActionKind actionKind, Runnable action) { - return command(actionKind, defaultPolicy, action); - } - - public boolean command(InventoryActionKind actionKind, InventorySwapPolicy policy, Runnable action) { - if (action == null) return false; - InventoryActionKind kind = actionKind != null ? actionKind : InventoryActionKind.GENERIC; - InventorySwapPolicy resolvedPolicy = policy != null ? policy : defaultPolicy; - - if (resolvedPolicy == InventorySwapPolicy.NONE || isSafeFor(kind, resolvedPolicy)) { - runGuarded(action); - return true; - } - - strictQueue.addLast(new QueuedAction( - kind, - resolvedPolicy, - action, - initialWaitTicks(resolvedPolicy, kind), - () -> isSafeFor(kind, resolvedPolicy) - )); - requestStrictMovementLock(); - return true; - } - - public boolean command(Predicate<ItemStack> predicate, InventoryActionKind actionKind, Runnable action) { - return execute(predicate, actionKind, action); - } - - public boolean command(Predicate<ItemStack> predicate, Runnable action) { - return execute(InventorySwapRequest.builder(predicate, action).build()); - } - - public boolean execute(Predicate<ItemStack> predicate, InventoryActionKind actionKind, Runnable action) { - return execute(InventorySwapRequest.builder(predicate, action) - .actionKind(actionKind) - .build()); - } - - public boolean executeSilent(Predicate<ItemStack> predicate, InventoryActionKind actionKind, Runnable action) { - return execute(InventorySwapRequest.builder(predicate, action) - .scope(InventorySearchScope.FULL) - .visibility(InventorySwapVisibility.SILENT) - .restore(true) - .actionKind(actionKind) - .build()); - } - - public boolean executeNormal(Predicate<ItemStack> predicate, InventoryActionKind actionKind, Runnable action) { - return execute(InventorySwapRequest.builder(predicate, action) - .scope(InventorySearchScope.FULL) - .visibility(InventorySwapVisibility.NORMAL) - .restore(false) - .actionKind(actionKind) - .build()); - } - - public boolean closeHandledScreen(InventorySwapPolicy policy) { - Minecraft mc = Minecraft.getInstance(); - LocalPlayer player = mc.player; - if (player == null || mc.getConnection() == null || player.containerMenu == null) return false; - - Runnable close = () -> mc.getConnection().send( - new ServerboundContainerClosePacket(player.containerMenu.containerId) - ); - - if (policy == null || policy == InventorySwapPolicy.NONE - || isSafeFor(InventoryActionKind.INVENTORY_CLOSE, policy)) { - runGuarded(close); - return true; - } - - strictQueue.addLast(new QueuedAction( - InventoryActionKind.INVENTORY_CLOSE, - policy, - close, - initialWaitTicks(policy, InventoryActionKind.INVENTORY_CLOSE), - () -> isSafeFor(InventoryActionKind.INVENTORY_CLOSE, policy) - )); - requestStrictMovementLock(); - return true; - } - - public InteractionResult useItem(InteractionHand hand) { - Minecraft mc = Minecraft.getInstance(); - LocalPlayer player = mc.player; - MultiPlayerGameMode interaction = mc.gameMode; - if (player == null || interaction == null || hand == null) return InteractionResult.FAIL; - return interaction.useItem(player, hand); - } - - public boolean useItemWith(Predicate<ItemStack> predicate, InventorySwapMode mode, InteractionHand hand, boolean swing) { - Minecraft mc = Minecraft.getInstance(); - return withFoundSlot(predicate, mode, () -> { - InteractionResult result = useItem(hand); - if (swing && result.consumesAction() && mc.player != null) { - mc.player.swing(hand); - } - }); - } - - public boolean useItemWith(Predicate<ItemStack> predicate, InventorySwapMode mode, InventorySwapPolicy policy, - InteractionHand hand, boolean swing) { - Minecraft mc = Minecraft.getInstance(); - return withFoundSlot(predicate, mode, policy, InventoryActionKind.USE_ITEM, () -> { - InteractionResult result = useItem(hand); - if (swing && result.consumesAction() && mc.player != null) { - swingHand(hand, policy); - } - }); - } - - public boolean swingHand(InteractionHand hand, InventorySwapPolicy policy) { - Minecraft mc = Minecraft.getInstance(); - if (mc.player == null || hand == null) return false; - Runnable swing = () -> mc.player.swing(hand); - if (policy == null || policy == InventorySwapPolicy.NONE || isSafeFor(InventoryActionKind.SWING, policy)) { - runGuarded(swing); - return true; - } - - strictQueue.addLast(new QueuedAction( - InventoryActionKind.SWING, - policy, - swing, - initialWaitTicks(policy, InventoryActionKind.SWING), - () -> isSafeFor(InventoryActionKind.SWING, policy) - )); - requestStrictMovementLock(); - return true; - } - - public SlotResult findHotbar(Predicate<ItemStack> predicate) { - Minecraft mc = Minecraft.getInstance(); - LocalPlayer player = mc.player; - if (player == null || predicate == null) return SlotResult.empty(); - - for (int slot = 0; slot < 9; slot++) { - ItemStack stack = player.getInventory().getItem(slot); - if (!stack.isEmpty() && predicate.test(stack)) { - return new SlotResult(slot, true, stack); - } - } - - return SlotResult.empty(); - } - - public SlotResult findInventory(Predicate<ItemStack> predicate) { - Minecraft mc = Minecraft.getInstance(); - LocalPlayer player = mc.player; - if (player == null || predicate == null) return SlotResult.empty(); - - for (int slot = 9; slot < 36; slot++) { - ItemStack stack = player.getInventory().getItem(slot); - if (!stack.isEmpty() && predicate.test(stack)) { - return new SlotResult(mapInventoryToScreenSlot(slot), true, stack); - } - } - - return SlotResult.empty(); - } - - private boolean runNormal(int slot, Runnable action) { - if (!isHotbarSlot(slot)) return false; - if (!selectHotbar(slot)) return false; - action.run(); - return true; - } - - private boolean runSilent(int slot, Runnable action) { - if (!isHotbarSlot(slot)) return false; - int previous = clientSelectedSlot(); - - if (lastServerSelectedSlot != slot) { - if (!selectHotbar(slot)) return false; - } - - try { - action.run(); - return true; - } finally { - if (lastServerSelectedSlot != previous) { - selectHotbar(previous); - } - } - } - - private boolean runInventoryNormal(int slot, Runnable action) { - if (isHotbarSlot(slot)) return runNormal(slot, action); - if (!clickSwap(slot)) return false; - action.run(); - return true; - } - - private boolean runInventorySilent(int slot, Runnable action) { - if (isHotbarSlot(slot)) return runSilent(slot, action); - int hotbarButton = clientSelectedSlot(); - if (!clickSwap(slot, hotbarButton)) return false; - - try { - action.run(); - return true; - } finally { - clickSwap(slot, hotbarButton); - } - } - - private InventorySearchScope resolveScope(InventorySwapRequest request) { - return request.scope() != null ? request.scope() : defaultScope; - } - - private InventorySwapVisibility resolveVisibility(InventorySwapRequest request) { - return request.visibility() != null ? request.visibility() : defaultVisibility; - } - - private InventorySwapMode resolveMode(InventorySwapRequest request, InventorySearchScope scope) { - InventorySearchScope safeScope = scope != null ? scope : defaultScope; - boolean restore = request.restore(restoreByDefault); - boolean silent = resolveVisibility(request) == InventorySwapVisibility.SILENT || restore; - - return switch (safeScope) { - case HOTBAR -> silent ? InventorySwapMode.SILENT : InventorySwapMode.NORMAL; - case INVENTORY -> silent ? InventorySwapMode.INVENTORY_SILENT : InventorySwapMode.INVENTORY_NORMAL; - case FULL -> silent ? InventorySwapMode.SILENT_FULL : InventorySwapMode.NORMAL_FULL; - }; - } - - private SlotResult findSlotForRequest(InventorySwapRequest request, InventorySwapMode mode, InventorySearchScope scope) { - if (scope != InventorySearchScope.FULL || preferHotbar) { - return findSlotForMode(request.predicate(), mode); - } - - SlotResult inventory = findInventory(request.predicate()); - return inventory.found() ? inventory : findHotbar(request.predicate()); - } - - private boolean isSafeFor(InventoryActionKind kind, InventorySwapPolicy policy) { - Minecraft mc = Minecraft.getInstance(); - if (policy == null || policy == InventorySwapPolicy.NONE) return true; - LocalPlayer player = mc.player; - if (player == null) return false; - - InventoryActionKind safeKind = kind != null ? kind : InventoryActionKind.GENERIC; - - if ((safeKind == InventoryActionKind.INVENTORY_CLICK || safeKind == InventoryActionKind.INVENTORY_CLOSE) - && isGrimMovingForInventory(player)) { - return false; - } - - if ((safeKind == InventoryActionKind.ATTACK_ENTITY || safeKind == InventoryActionKind.SWING) - && player.isUsingItem()) { - return false; - } - - if ((safeKind == InventoryActionKind.ATTACK_ENTITY || safeKind == InventoryActionKind.ENTITY_INTERACT) - && lastBlockActionTick == packetTick) { - return false; - } - - if ((safeKind == InventoryActionKind.BLOCK_INTERACT || safeKind == InventoryActionKind.BLOCK_BREAK) - && lastEntityActionTick == packetTick) { - return false; - } - - return safeKind != InventoryActionKind.SWING || lastUseItemTick != packetTick; - } - - private boolean isSafeForSwap(int slot, InventorySwapMode mode, InventoryActionKind kind, InventorySwapPolicy policy) { - if (!isSafeFor(kind, policy)) return false; - if (requiresInventoryClick(slot, mode)) { - return isSafeFor(InventoryActionKind.INVENTORY_CLICK, policy); - } - return true; - } - - private boolean requiresInventoryClick(int slot, InventorySwapMode mode) { - if (mode == null) return false; - return switch (mode) { - case INVENTORY_NORMAL, INVENTORY_SILENT -> !isHotbarSlot(slot); - case SILENT_FULL, NORMAL_FULL -> !isHotbarSlot(slot); - case NORMAL, SILENT -> false; - }; - } - - private int initialWaitTicks(InventorySwapPolicy policy, InventoryActionKind kind) { - if (policy == InventorySwapPolicy.LEGIT) return legitWaitTicks; - if (kind == InventoryActionKind.INVENTORY_CLICK || kind == InventoryActionKind.INVENTORY_CLOSE) { - return strictInventoryWaitTicks; - } - return 0; - } - - private boolean isGrimMovingForInventory(LocalPlayer player) { - if (isMovementLocked()) return false; - if (player.isSprinting() && (!player.isSwimming() || player.onGround())) return true; - if (lastInputMoving) return true; - if (player.input == null) return false; - Input input = player.input.keyPresses; - if (input == null) return false; - return input.forward() || input.backward() || input.left() || input.right(); - } - - private void runGuarded(Runnable action) { - beginInternalSwap(); - try { - action.run(); - } finally { - endInternalSwap(); - } - } - - private void requestStrictMovementLock() { - requestMovementLock(strictMovementLockTicks); - } - - private void requestMovementLock(int ticks) { - Minecraft mc = Minecraft.getInstance(); - movementLockTicks = Math.max(movementLockTicks, Math.max(0, ticks)); - if (mc.player != null && mc.player.isSprinting()) { - mc.player.setSprinting(false); - } - } - - private boolean isMovementLocked() { - return movementLockTicks > 0 || !strictQueue.isEmpty(); - } - - private HotbarLease activeLease() { - Minecraft mc = Minecraft.getInstance(); - HotbarLease current = hotbarLease; - if (current == null) return null; - - LocalPlayer player = mc.player; - if (player == null) { - hotbarLease = null; - return null; - } - - if (!player.getUUID().equals(current.playerUuid) || player.tickCount >= current.resetAfterAge) { - clearLease(current, player.getUUID().equals(current.playerUuid)); - return null; - } - - return current; - } - - private void clearLease(HotbarLease lease, boolean restoreServerSlot) { - hotbarLease = null; - if (!restoreServerSlot) return; - - int currentClientSlot = clientSelectedSlot(); - if (currentClientSlot != lease.enforcedSlot) { - sendSelectedSlotIfNeeded(currentClientSlot); - } - } - - private void beginInternalSwap() { - internalSwapDepth++; - } - - private void endInternalSwap() { - if (internalSwapDepth > 0) internalSwapDepth--; - } - - private void syncInteractionManagerSlot(int slot) { - Minecraft mc = Minecraft.getInstance(); - if (mc.gameMode instanceof MultiPlayerGameModeAccessor accessor) { - accessor.lovisual$setLastSelectedSlot(slot); - } - } - - private record HotbarLease(UUID playerUuid, Object owner, int enforcedSlot, int clientSlot, int resetAfterAge) { - } - - private static final class QueuedAction { - private final InventoryActionKind kind; - private final InventorySwapPolicy policy; - private final Runnable action; - private final BooleanSupplier safety; - private int waitTicks; - - private QueuedAction(InventoryActionKind kind, InventorySwapPolicy policy, Runnable action, int waitTicks, - BooleanSupplier safety) { - this.kind = kind != null ? kind : InventoryActionKind.GENERIC; - this.policy = policy != null ? policy : InventorySwapPolicy.NONE; - this.action = action; - this.safety = safety != null ? safety : () -> true; - this.waitTicks = Math.max(0, waitTicks); - } - - private boolean isSafe() { - return safety.getAsBoolean(); - } - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/inventory/core/InventorySwapMode.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/inventory/core/InventorySwapMode.java deleted file mode 100644 index 1c20496..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/inventory/core/InventorySwapMode.java +++ /dev/null @@ -1,17 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.util.player.inventory.core; - -public enum InventorySwapMode { - NORMAL, - SILENT, - INVENTORY_SILENT, - INVENTORY_NORMAL, - SILENT_FULL, - NORMAL_FULL -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/inventory/core/InventorySwapPolicy.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/inventory/core/InventorySwapPolicy.java deleted file mode 100644 index 62e0133..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/inventory/core/InventorySwapPolicy.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.util.player.inventory.core; - -import dev.loki.lovisual.config.values.mode.EnumValue; - -public enum InventorySwapPolicy implements EnumValue.IdProvider { - NONE("none"), - GRIM_STRICT("grim_strict"), - LEGIT("legit"); - - private final String id; - - InventorySwapPolicy(String id) { - this.id = id; - } - - @Override - public String id() { - return id; - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/inventory/core/InventorySwapRequest.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/inventory/core/InventorySwapRequest.java deleted file mode 100644 index 18d1a1b..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/inventory/core/InventorySwapRequest.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.util.player.inventory.core; - -import net.minecraft.world.item.ItemStack; - -import java.util.Objects; -import java.util.function.Predicate; -import dev.loki.lovisual.util.player.inventory.core.InventorySwapPolicy; -import dev.loki.lovisual.util.player.inventory.support.InventoryActionKind; -import dev.loki.lovisual.util.player.inventory.support.InventorySearchScope; -import dev.loki.lovisual.util.player.inventory.support.InventorySwapVisibility; - -public final class InventorySwapRequest { - private final Predicate<ItemStack> predicate; - private final InventorySearchScope scope; - private final InventorySwapVisibility visibility; - private final Boolean restore; - private final InventorySwapPolicy policyOverride; - private final InventoryActionKind actionKind; - private final Runnable action; - - private InventorySwapRequest(Builder builder) { - this.predicate = builder.predicate; - this.scope = builder.scope; - this.visibility = builder.visibility; - this.restore = builder.restore; - this.policyOverride = builder.policyOverride; - this.actionKind = builder.actionKind; - this.action = builder.action; - } - - public static Builder builder(Predicate<ItemStack> predicate, Runnable action) { - return new Builder(predicate, action); - } - - public Predicate<ItemStack> predicate() { - return predicate; - } - - public InventorySearchScope scope() { - return scope; - } - - public InventorySwapVisibility visibility() { - return visibility; - } - - public boolean restore(boolean defaultRestore) { - return restore != null ? restore : defaultRestore; - } - - public InventorySwapPolicy policy(InventorySwapPolicy defaultPolicy) { - return policyOverride != null ? policyOverride : defaultPolicy; - } - - public InventoryActionKind actionKind() { - return actionKind; - } - - public Runnable action() { - return action; - } - - public static final class Builder { - private final Predicate<ItemStack> predicate; - private final Runnable action; - private InventorySearchScope scope; - private InventorySwapVisibility visibility; - private Boolean restore; - private InventorySwapPolicy policyOverride; - private InventoryActionKind actionKind = InventoryActionKind.GENERIC; - - private Builder(Predicate<ItemStack> predicate, Runnable action) { - this.predicate = Objects.requireNonNull(predicate, "predicate"); - this.action = Objects.requireNonNull(action, "action"); - } - - public Builder scope(InventorySearchScope scope) { - this.scope = scope; - return this; - } - - public Builder visibility(InventorySwapVisibility visibility) { - this.visibility = visibility; - return this; - } - - public Builder restore(boolean restore) { - this.restore = restore; - return this; - } - - public Builder policy(InventorySwapPolicy policy) { - this.policyOverride = policy; - return this; - } - - public Builder actionKind(InventoryActionKind actionKind) { - this.actionKind = actionKind != null ? actionKind : InventoryActionKind.GENERIC; - return this; - } - - public InventorySwapRequest build() { - return new InventorySwapRequest(this); - } - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/inventory/firework/FireworkUseController.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/inventory/firework/FireworkUseController.java index 1cb3635..34fc3c6 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/inventory/firework/FireworkUseController.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/inventory/firework/FireworkUseController.java @@ -7,6 +7,8 @@ package dev.loki.lovisual.util.player.inventory.firework; +import dev.loki.lovisual.mixins.accessors.inventory.PlayerInventoryAccessor; +import dev.loki.lovisual.mixins.accessors.misc.MultiPlayerGameModeAccessor; import net.minecraft.client.Minecraft; import net.minecraft.client.multiplayer.MultiPlayerGameMode; import net.minecraft.client.player.LocalPlayer; @@ -15,17 +17,9 @@ import net.minecraft.world.InteractionResult; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.projectile.FireworkRocketEntity; import net.minecraft.world.item.Items; -import dev.loki.lovisual.util.player.inventory.core.InventorySwap; -import dev.loki.lovisual.util.player.inventory.core.InventorySwapRequest; -import dev.loki.lovisual.util.player.inventory.support.InventoryActionKind; -import dev.loki.lovisual.util.player.inventory.support.InventorySearchScope; -import dev.loki.lovisual.util.player.inventory.support.InventorySwapVisibility; + /** - * Shared firework-use path for elytra modules. - * <p> - * The important part is not the delay itself. Firework use is routed through - * InventorySwap, so internal selected-slot/swing packets are marked as inventory - * controller activity and do not poison AttackPressing/KillAura cooldown timing. + * Shared firework-use path for elytra modules — hotbar-only, без InventorySwap. */ public final class FireworkUseController { public static final FireworkUseController INSTANCE = new FireworkUseController(); @@ -59,7 +53,7 @@ public final class FireworkUseController { if (player.getOffhandItem().is(Items.FIREWORK_ROCKET)) { used = useWithTemporaryRotation(player, yaw, pitch, InteractionHand.OFF_HAND); } else { - used = useMainHandRocket(player, yaw, pitch, allowInventory, restore, silent); + used = useMainHandRocket(player, yaw, pitch, restore); } if (used) { @@ -78,40 +72,53 @@ public final class FireworkUseController { return !canPassCooldown(cooldownTicks); } - private boolean useMainHandRocket(LocalPlayer player, - float yaw, - float pitch, - boolean allowInventory, - boolean restore, - boolean silent) { - InventorySearchScope scope = allowInventory ? InventorySearchScope.FULL : InventorySearchScope.HOTBAR; - boolean[] accepted = {false}; + private boolean useMainHandRocket(LocalPlayer player, float yaw, float pitch, boolean restore) { + int slot = findFireworkInHotbar(player); + if (slot == -1) return false; + Minecraft mc = Minecraft.getInstance(); + int original = ((PlayerInventoryAccessor) player.getInventory()).lovisual$getSelectedSlot(); + if (slot != original) { + ((PlayerInventoryAccessor) player.getInventory()).lovisual$setSelectedSlot(slot); + syncSlot(slot); + } + boolean ok = useWithTemporaryRotation(player, yaw, pitch, InteractionHand.MAIN_HAND); + if (restore && slot != original) { + ((PlayerInventoryAccessor) player.getInventory()).lovisual$setSelectedSlot(original); + syncSlot(original); + } + return ok; + } - InventorySwapRequest request = InventorySwapRequest - .builder(stack -> stack.is(Items.FIREWORK_ROCKET), () -> { - accepted[0] = useWithTemporaryRotation(player, yaw, pitch, InteractionHand.MAIN_HAND); - }) - .scope(scope) - .visibility(silent ? InventorySwapVisibility.SILENT : InventorySwapVisibility.NORMAL) - .restore(restore) - .actionKind(InventoryActionKind.USE_ITEM) - .build(); + private int findFireworkInHotbar(LocalPlayer player) { + for (int i = 0; i < 9; i++) { + if (player.getInventory().getItem(i).is(Items.FIREWORK_ROCKET)) return i; + } + return -1; + } - return InventorySwap.INSTANCE.command(request) && accepted[0]; + 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)); } private boolean useWithTemporaryRotation(LocalPlayer player, float yaw, float pitch, InteractionHand hand) { + Minecraft mc = Minecraft.getInstance(); + MultiPlayerGameMode manager = mc.gameMode; + if (manager == null) return false; float oldYaw = player.getYRot(); float oldPitch = player.getXRot(); player.setYRot(yaw); player.setXRot(pitch); - try { - InteractionResult result = InventorySwap.INSTANCE.useItem(hand); + InteractionResult result = manager.useItem(player, hand); if (!result.consumesAction()) { return false; } - InventorySwap.INSTANCE.swingHand(hand, InventorySwap.INSTANCE.defaultPolicy()); + player.swing(hand); return true; } finally { player.setYRot(oldYaw); @@ -121,6 +128,7 @@ public final class FireworkUseController { private boolean hasOwnedActiveFirework(LocalPlayer player) { Minecraft mc = Minecraft.getInstance(); + if (mc.level == null) return false; for (Entity entity : mc.level.entitiesForRendering()) { if (entity instanceof FireworkRocketEntity firework && firework.isAlive() @@ -132,7 +140,6 @@ public final class FireworkUseController { } private boolean isKillAuraAttackWindowBusy() { - // KillAura module removed; no combat-window blocking remains. return false; } } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/inventory/support/InventoryActionKind.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/inventory/support/InventoryActionKind.java deleted file mode 100644 index 5896929..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/inventory/support/InventoryActionKind.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.util.player.inventory.support; - -public enum InventoryActionKind { - GENERIC, - INVENTORY_CLICK, - INVENTORY_CLOSE, - USE_ITEM, - SWING, - ATTACK_ENTITY, - ENTITY_INTERACT, - BLOCK_INTERACT, - BLOCK_BREAK -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/inventory/support/InventorySearchScope.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/inventory/support/InventorySearchScope.java deleted file mode 100644 index 34854e6..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/inventory/support/InventorySearchScope.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.util.player.inventory.support; - -import dev.loki.lovisual.config.values.mode.EnumValue; - -public enum InventorySearchScope implements EnumValue.IdProvider { - HOTBAR("hotbar"), - INVENTORY("inventory"), - FULL("full"); - - private final String id; - - InventorySearchScope(String id) { - this.id = id; - } - - @Override - public String id() { - return id; - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/inventory/support/InventorySwapVisibility.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/inventory/support/InventorySwapVisibility.java deleted file mode 100644 index 300c81a..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/inventory/support/InventorySwapVisibility.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.util.player.inventory.support; - -import dev.loki.lovisual.config.values.mode.EnumValue; - -public enum InventorySwapVisibility implements EnumValue.IdProvider { - NORMAL("normal"), - SILENT("silent"); - - private final String id; - - InventorySwapVisibility(String id) { - this.id = id; - } - - @Override - public String id() { - return id; - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/inventory/support/SlotResult.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/inventory/support/SlotResult.java deleted file mode 100644 index 36cdb92..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/inventory/support/SlotResult.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.util.player.inventory.support; - -import net.minecraft.world.item.ItemStack; - -public record SlotResult(int slot, boolean found, ItemStack stack) { - public static SlotResult empty() { - return new SlotResult(-1, false, ItemStack.EMPTY); - } - - public boolean isHotbar() { - return found && slot >= 0 && slot <= 8; - } - - public boolean isInventory() { - return found && slot > 8; - } -} diff --git a/combatant-client-26.2/src/main/resources/lovisual.mixins.json b/combatant-client-26.2/src/main/resources/lovisual.mixins.json index 939f3fb..fe625ed 100644 --- a/combatant-client-26.2/src/main/resources/lovisual.mixins.json +++ b/combatant-client-26.2/src/main/resources/lovisual.mixins.json @@ -140,11 +140,11 @@ "network.game.PlayerInfoMixin", "network.packet.ClientCommonPacketListenerImplMixin", "network.packet.ServerboundUseItemPacketMixin", - "render.entity.EndCrystalModelMixin", - "render.entity.EndCrystalModelTintMixin", + "render.entity.crystal.EndCrystalModelMixin", + "render.entity.crystal.EndCrystalModelTintMixin", "render.entity.EntityRenderDispatcherMixin", - "render.entity.ItemEntityRenderStateMixin", - "render.entity.ItemEntityRendererMixin", + "render.entity.item.ItemEntityRenderStateMixin", + "render.entity.item.ItemEntityRendererMixin", "render.entity.LivingEntityRenderStateMixin", "render.fog.AtmosphericFogEnvironmentMixin", "render.fog.FogRendererMixin", @@ -156,9 +156,9 @@ "render.item.ItemInHandAlwaysVisibleMixin", "render.item.ItemInHandRendererMixin", "render.level.CameraMixin", - "render.level.FireworkParticlesStarterMixin", + "render.level.particles.FireworkParticlesStarterMixin", "render.level.LevelRendererMixin", - "render.level.ParticleManagerMixin", + "render.level.particles.ParticleManagerMixin", "render.level.SkyRendererMixin", "render.light.LightmapMixin", "render.light.LightmapRenderStateExtractorMixin", diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/command/impl/world/XaeroWaypointCommandTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/command/impl/world/XaeroWaypointCommandTest.java index 75a6b60..badf023 100644 --- a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/command/impl/world/XaeroWaypointCommandTest.java +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/command/impl/world/XaeroWaypointCommandTest.java @@ -11,7 +11,7 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; -import dev.loki.lovisual.features.command.impl.world.XaeroWaypointCommand; +import dev.loki.lovisual.features.command.impl.world.integration.XaeroWaypointCommand; public class XaeroWaypointCommandTest { @Test diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/AlwaysVisibleHandTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/AlwaysVisibleHandTest.java index 88f403b..0f6c307 100644 --- a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/AlwaysVisibleHandTest.java +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/AlwaysVisibleHandTest.java @@ -10,7 +10,7 @@ package dev.loki.lovisual.features.module.modules.visuals; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import dev.loki.lovisual.features.module.modules.visuals.items.AlwaysVisibleHand; +import dev.loki.lovisual.features.module.modules.visuals.items.held.AlwaysVisibleHand; public final class AlwaysVisibleHandTest { diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/AtmoDawnFogTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/AtmoDawnFogTest.java index 8cd92d9..643569e 100644 --- a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/AtmoDawnFogTest.java +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/AtmoDawnFogTest.java @@ -11,7 +11,7 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -import dev.loki.lovisual.features.module.modules.visuals.weather.AtmoDawnFog; +import dev.loki.lovisual.features.module.modules.visuals.weather.sky.AtmoDawnFog; /** * Чистая математика AtmoDawnFog: окно времени суток и высотное затухание. diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/EaseCurvesTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/EaseCurvesTest.java index eb007f0..082e491 100644 --- a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/EaseCurvesTest.java +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/EaseCurvesTest.java @@ -11,7 +11,7 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -import dev.loki.lovisual.features.module.modules.visuals.items.HoldMyItems; +import dev.loki.lovisual.features.module.modules.visuals.items.held.HoldMyItems; /** * Математика ease-out-back для HoldMyItems. diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/EquipProgressComposerTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/EquipProgressComposerTest.java index 64aaa16..508aed5 100644 --- a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/EquipProgressComposerTest.java +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/animation/EquipProgressComposerTest.java @@ -12,7 +12,7 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import dev.loki.lovisual.features.module.modules.visuals.camera.ViewModel; -import dev.loki.lovisual.features.module.modules.visuals.items.HoldMyItems; +import dev.loki.lovisual.features.module.modules.visuals.items.held.HoldMyItems; /** * Совместная работа HoldMyItems (кривая) и ViewModel (занижение):