diff --git a/.gitignore b/.gitignore index 6c51d10..21ae973 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ ref/ # IDE/Build artifacts .factorypath **/bin/ +.claude/ +docs/ +.ruff_cache/ diff --git a/TODO.md b/TODO.md index 8757caa..2580046 100644 --- a/TODO.md +++ b/TODO.md @@ -28,12 +28,13 @@ - [x] Фаза 4: Security (BackdoorProtection, StaffTracker) - [x] Фаза 5: Нормализация модулей (ленивый Minecraft, GameClock) - [x] Фаза 6: Тесты (EventBus, конфиг, утилиты) — см. `combatant-client-26.2/TODO.md` -- [ ] Фаза 7: Новые модули по мотивам SoupVisuals — см. ниже +- [ ] Фаза 7: Новые модули по мотивам SoupVisuals — Cosmetics Overhaul ЗАВЕРШЁН 2026-09-11, + см. ниже (остатки: Gps, HitBubbles, AmbientParticles, EndCrystal, Trinket, Balancer, HandsShader) - [ ] Фаза 8.5: Аудит структуры ≤200/≤4 — 8.5.1 (папки >4) ЗАВЕРШЕНА 2026-09-10 (0 папок >4, коммиты 8846d53…303b17e); далее 8.5.2 (гиганты >600), план в `combatant-client-26.2/TODO.md` -- [ ] Фаза 9: Оптимизация FPS (9.1 фиксы мёртвых грузов → 9.2 модуль Optimize), - план в `combatant-client-26.2/TODO.md` +- [ ] Фаза 9: Оптимизация FPS — 9.1 (мёртвые грузы) ЗАВЕРШЕНА 2026-09-11 (137121e…4d60d3d); + 9.2 Optimize гейтится Фазой 8.5, 9.3 A/B-мерка needs запуск на Windows --- @@ -63,87 +64,21 @@ иконки-спутники (Atom/Ball/Cat/Donut/Flame/Flower/Gem/Ghost/MCTiers), трейл (цвет sync/custom, длина, толщина), звуки (типы + громкость), частицы (у Soup 315 стр.). Референс: `other/Trinket.java` -- [ ] **Cosmetics Overhaul (Unified Cosmetics)** — сейчас модуль бесполезен (next/prev - keybind без экрана). План (обсуждён в брейнсторме, черновик, детали могут - измениться): - - **Слоты вместо типов**: новый `CosmeticSlot` enum — `MODEL, PET, HEAD, - SHOULDERS, BACK, WAIST, LEGS, HELD` (8 шт). `CosmeticType` (MODEL/PET/ACCESSORY) - остаётся внутри `CosmeticModel` как есть (анимация/рендер-логика не трогается). - - Аксессуары (ACCESSORY) получают поле `slot` — парсится из `avatar.json` - (`"slot": "head"`), при отсутствии — вывод по имени папки (существующая - эвристика witch/hat→HEAD, wing→BACK, wheel→WAIST, иначе→HELD). - - `CosmeticManager`: `EnumMap` вместо 3 полей. - MODEL/PET — по одному активному. 6 accessory-слотов — **можно носить - несколько одновременно** (шляпа + крылья + пояс разом). - - Экран `CosmeticsScreen`: левая панель — живая 3D-модель игрока - (`InventoryScreen.renderEntityInInventoryFollowsMouse` или свой вызов - EntityRenderDispatcher), drag ЛКМ по превью — вращение (yaw/pitch, - pitch ограничен ±30°), скролл — zoom (0.5–2.0), кнопка сброса угла. - Открывается через **ПКМ по модулю в ClickGUI** (спецкейс вместо обычной - settings-панели — прецедент есть в `ClickGuiEditorScreen`). - - Hotspot-зоны поверх 3D-модели (HEAD/SHOULDERS/BACK/WAIST/LEGS + иконка - HELD сбоку) — клик открывает правую панель выбора модели для этого слота. - - Offset/rotation/scale — **на слот** (не на модель, т.к. модулей может быть - десятки, а статичных SettingDef-полей на каждую не сделать): 6 стандартных - num()-хелперов модуля (per-slot), visibleWhen(активный слот). Смена модели - в слоте может потребовать подстройки — ок, вписывается в существующий - SettingDef/SettingFactory конвейер без новой инфраструктуры. - - Цвета из Theme.theme() (windowBg/accent/textMuted), файлы по правилам - ≤200 строк/≤4 файла на папку: `CosmeticsModule`, `CosmeticsScreen`, - `CosmeticsSlot`(=CosmeticSlot), `CosmeticsSlotPanel`. - - i18n: `module.cosmetics.*`, `setting.cosmetics.active_slot`, - `offset_x/y/z`, `rotation`, `scale`, `setting.cosmetics.slot.` и т.д. - (en_us + ru_ru обязательно). - - Открытые вопросы для следующего захода: точные хитбокс-координаты зон - по проценту региона превью, формат хранения per-slot offset в .lvcfg, - нужен ли предпросмотр модели до эквипа в правой панели. - - **Портирование процедурных head/back-модулей в слоты** — из всех - `visuals/*` только 3 модуля реально являются «надетыми на игрока» - процедурными косметиками (нашёл через grep по HeadAnchor-хелперу): - - `ChinaHat` (+`ChinaHatModel`, `ChinaHatPalette`, 295+107+144 стр.) — - конус на голове → встроенная опция слота **HEAD**. - - `TazikHat` (859 стр.! требует разбивки на файлы ≤200 стр. при - портировании — геометрия/палитра/модуль отдельно, как у ChinaHat) — - «тазик»/чаша на голове → **HEAD**. - - `BackSword` (287 стр.) — катана на спине → **BACK**. - Остальные visuals-модули (ESP, Chams, Trails, Rain, PostFX и т.д.) — - НЕ переносятся, это общие эффекты, а не надеваемые предметы. - План: каждый становится встроенным вариантом (built-in entry, не - .bbmodel) внутри соответствующего слота — юзер выбирает между - своими .bbmodel-моделями И этими процедурными built-in вариантами - в одном и том же слоте. Настройки самого варианта (цвет/размер/сегменты - и т.д.) показываются в правой панели слота, когда built-in выбран - активным. Старые `@ModuleInfo`-модули `chinahat`/`tazikhat`/`backsword` - удаляются как отдельные модули из списка Visuals (их функциональность - полностью переезжает в Cosmetics) — конфиг-миграция/бэккомпат не нужны - (легит-клиент, публичного релиза со старыми конфигами ещё не было — - уточнить у пользователя перед удалением). - - **"None" в каждом слоте** — обязательный первый вариант в списке выбора - любого слота (Model/Pet/Head/Shoulders/Back/Waist/Legs/Held): "ничего - не надето", слот пуст, `CosmeticManager.active` для этого слота = null. - Не путать с выключенным модулем `CosmeticDisplay` целиком — None это - per-slot состояние, остальные слоты продолжают рендериться. - - Подтверждено пользователем: точно только 3 built-in кандидата - (ChinaHat/TazikHat/BackSword), остальные visuals-модули не переносятся. - - **Решённые вопросы (финал черновика):** - 1. Hotspot-координаты — **вариант B**: проекция костей игрока (голова/тело/ - ноги vanilla-модели) в экранные координаты через `ScreenProjection` - (уже есть в `render/helpers/`), а не фиксированные проценты региона — - точнее держит зоны при вращении/приседе. Fallback на фикс-проценты, - если в реализации окажется дороже, чем стоит. - 2. Хранение offset/rot/scale — **обычные `NumberValue`-поля модуля** - (6 слотов × offset_x/y/z/rot/scale), сохраняются в `.lvcfg` как - обычные настройки — без нового ConfigValue-типа. - 3. Правая панель — **live-apply**: клик по модели в списке слота сразу - применяет и показывает на превью, без отдельной кнопки Apply. - 4. Built-in процедурные варианты (ChinaHat/TazikHat/BackSword) — **просто - обычные записи в списке слота** (с иконкой-превью), не выделяются в - отдельную секцию; настройки открываются так же, как у .bbmodel-моделей. - 5. `TazikHat` (859 стр.) при портировании — **разбить как ChinaHat**: - `TazikHatGeometry` + `TazikHatPalette` + встраиваемый рендер-класс без - собственного `@ModuleInfo`/Module-обвязки (вызывается из слота HEAD). - 6. Старые модули `chinahat`/`tazikhat`/`backsword` — **удаляются - полностью** из списка Visuals, функциональность только в Cosmetics. +- [x] **Cosmetics Overhaul (Unified Cosmetics)** — ЗАВЕРШЁН 2026-09-11: + слоты `CosmeticSlot` (MODEL/PET/HEAD/SHOULDERS/BACK/WAIST/LEGS/HELD), парсинг + `slot` из avatar.json + эвристика по имени папки, `CosmeticManager` на EnumMap + (аксессуары — несколько одновременно), `CosmeticsScreen` (3D-превью drag/zoom + + хотспоты + правая панель live-apply с "None" первым), per-slot offset/rot/scale + через NumberValue-поля, built-in ChinaHat/TazikHat/BackSword в `cosmetics/builtin/` + (старые модули chinahat/tazikhat/backsword удалены), i18n en+ru, тесты + CosmeticSlotTest/CosmeticsHotspotTest. + - ~~CosmeticDisplay 232 / TazikHatRenderer 242 / BackSwordRenderer 231 >200~~ — + разбиты: CosmeticDisplay 196 + CosmeticBuiltins (значения+диспетчер built-in), + TazikHat 186 (палитра-математика переиспользует ChinaHatPalette), BackSword 200 + (edge/glow слиты в параметризованный drawKatana). + - Не сделано из плана (следующий заход): иконки-превью built-in в списке слота, + правка слотов SHOULDERS/LEGS/PET из экрана (сейчас только HEAD/BACK built-in), + предпросмотр до эквипа. - [ ] **Balancer** (mctiers) — тиры игроков с mctiers.info (у Soup отдельная категория MCTiers). Референс: `mctiers/Balancer.java` - [ ] **HandsShader: новые шейдеры руки** — у Soup 10 режимов (Solid, Chroma, Balatro, diff --git a/combatant-client-26.2/TODO.md b/combatant-client-26.2/TODO.md index 7f2f519..cd54f0c 100644 --- a/combatant-client-26.2/TODO.md +++ b/combatant-client-26.2/TODO.md @@ -315,7 +315,8 @@ - [ ] ModulesMenuScreen 960 — рендер категорий отдельно от состояния - [ ] UiMeshGeometry 954 — по примитивам геометрии - [ ] BetterTooltips 953, OrderedUiBatcher 935 (→ batches + blur), Scoreboard 924, - Rain 893 (+CompactHudStatModel 893), TazikHat 859 (обещан в Фазе 7 — разбить при портировании), + Rain 893 (+CompactHudStatModel 893), TazikHat 859 ✓ закрыт портированием в + Cosmetics built-in (2026-09-11, см. Фазу 7), ShaderPatchCompiler 843, OnlineRelationPlayerPickerComponent 837, CustomTabList 830, GameRendererMixin 810 (только хуки; тело вынести в `render/engine/entry/`-классы — ВНИМАНИЕ: 9.1 тоже его трогает, координировать), Predictions 805 @@ -351,30 +352,30 @@ > вместо неё — ручной модуль **Optimize** (включил → порезали стекло/blur/тяжёлые проходы). ### 9.1 Мёртвые грузы (чиним в коде всегда, независимо от модулей) — приоритет -- [ ] **glGetError на каждом render pass** — `GlStencilShapeClipBackend.endRenderPass` +- [x] **glGetError на каждом render pass** — ГОТОВО 2026-09-11 (коммит 137121e): гейт по clip-active || stencil-debug — `GlStencilShapeClipBackend.endRenderPass` (`:140`) и `beginRenderPass` (`:98/:104/:110/:124`) дёргают `checkGlErrors` (`:428-434`) на КАЖДЫЙ проход (миксин `CommandEncoderMixin:102-112` висит на submitRenderPass → это и все ванильные/Sodium проходы). `glGetError` = синк с Windows-драйвером, несколько мс/кадр. Фикс: проверять только если `mode != DISABLED || renderPassAttachmentRequired`, либо под dev-флагом; в релизе — no-op. -- [ ] **`lovisual$isVulkanBackend()` на каждый submitRenderPass** +- [x] **`lovisual$isVulkanBackend()` на каждый submitRenderPass** — ГОТОВО (c9a65b1), кэш Boolean (`CommandEncoderMixin:66-74`) — `getDeviceInfo().backendName().toLowerCase()` заново на кадр×проходы. Фикс: вычислить один раз, закэшировать boolean. -- [ ] **PreTranslucentDepth — полноэкранный depth-copy каждый кадр без нужды** — +- [x] **PreTranslucentDepth — полноэкранный depth-copy каждый кадр без нужды** — ГОТОВО (b747b47): demand-флаг requestForNextFrame()/takeRequested() у потребителей getDepthView(For), capture в vanilla- и Iris-путях по запросу (1-кадр задержка, фолбэк на live view) — `LevelRendererMixin:275-283` гейтится только «нет Iris». Фикс: гейт `needsPreTranslucentDepthCapture()` по образцу `WorldSceneDepth` (`LevelRendererMixin:160-218`); потребители: `PostProcessGraphResources:61`, `LoVisualVisuals:79`, `WorldPassCompiler:174`. -- [ ] **copyMainColor каждый кадр ДО гейта** — `UiBlurResources.captureWorldSource` +- [x] **copyMainColor каждый кадр ДО гейта** — ГОТОВО (118eb2a): ранний return по liquidGlassBlurRequested — `UiBlurResources.captureWorldSource` (`:116-123`): полноэкранный color-copy делается до проверки `liquidGlassBlurRequested` (`:122`). Фикс: переставить — copy только когда blur реально запрошен. -- [ ] **Строки профайлера при выключенном профайлере** — конкатенации на call-site +- [x] **Строки профайлера при выключенном профайлере** — ГОТОВО (aa55c4d): 2-arg scope()/section()/postPass(prefix,suffix) в ProfilerPhase/RenderProfiler2D/3D/RenderCostProfiler (DEV-гейт конкатенации) — конкатенации на call-site каждый кадр: `PostProcessGraph:59` (`"graph:" + phase`), `ModuleManager:219-221/:290` (`"3d:module:" + m.name()`). Фикс: ленивые supplier'ы / ранний isEnabled-check. -- [ ] **Мелочь, по ходу**: `new PoseStack`/`new Matrix4f`×2/`new Frustum`×2 в кадре - (`GameRendererMixin:203-239/:341-347/:609-617`) — закэшить переиспользуемые копии. +- [x] **Мелочь, по ходу**: `new PoseStack`/`new Matrix4f`×2/`new Frustum`×2 в кадре + (`GameRendererMixin:203-239/:341-347/:609-617`) — закэшить переиспользуемые копии. Frustum-часть отменена (da6525c): @Invoker("calculateFrustum") не резолвится на remapped-рантайме = краш; scratch-кэш матриц/PoseStack остался. ### 9.2 Модуль **Optimize** (misc) — ручной рубильник экономии > ⛔ ГЕЙТ: не начинать, пока не закрыта Фаза 8.5 (рефакторинг-аудитStructure ниже) — 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 424d7ad..348f990 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 @@ -42,21 +42,17 @@ public final class MainConfig implements ConfigObject, ConfigNameProvider, Setti private final ModeValue debug = new ModeValue("debug", "off", "off", "error_only", "error_and_warnings", "info", "config", "render_thread", "stencil", "serverdebug", "all"); - //@CFGComment("Force PvP mode ON everywhere (dev/testing only)") - private final BooleanValue forcePvp = - new BooleanValue("forcePvp", false); - //@CFGComment("Menu background shader: off / waves / aurora") private final ModeValue menuBg = - new ModeValue("menuBg", "off", "off", "waves", "aurora"); + new ModeValue("menuBg", "waves", "off", "waves", "aurora"); //@CFGComment("Use ClickGUI theme colors for menu background shader") private final BooleanValue menuBgUseTheme = - new BooleanValue("menuBgUseTheme", false); + new BooleanValue("menuBgUseTheme", true); //@CFGComment("Show seconds in the custom main menu clock") private final BooleanValue menuClockShowSeconds = - new BooleanValue("menuClockShowSeconds", false); + new BooleanValue("menuClockShowSeconds", true); //@CFGComment("MSAA for world render: off / 2x / 4x") private final ModeValue msaa3d = @@ -192,10 +188,6 @@ public final class MainConfig implements ConfigObject, ConfigNameProvider, Setti DebugLog.setMode(getDebugMode()); } - public boolean isForcePvp() { - return forcePvp.get(); - } - public String getMenuBackgroundMode() { return menuBg.get(); } @@ -365,7 +357,6 @@ public final class MainConfig implements ConfigObject, ConfigNameProvider, Setti public List getMiscellaneousSettingDefs() { List list = new ArrayList<>(); list.add(SettingDef.mode(debug)); - list.add(SettingDef.bool(forcePvp)); list.add(SettingDef.bool(disableNarrator)); return list; } @@ -440,7 +431,6 @@ public final class MainConfig implements ConfigObject, ConfigNameProvider, Setti public List> getConfigValues() { List> list = new ArrayList<>(); list.add(debug); - list.add(forcePvp); list.add(menuBg); list.add(menuClockShowSeconds); list.add(menuBgUseTheme); 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/ParticleSpawnEvent.java new file mode 100644 index 0000000..c169278 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/events/impl/render/ParticleSpawnEvent.java @@ -0,0 +1,41 @@ +/* + * 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.events.impl.render; + +import net.minecraft.core.particles.ParticleOptions; +import dev.loki.lovisual.events.Event; + +public class ParticleSpawnEvent extends Event { + private final ParticleOptions options; + private final double x; + private final double y; + private final double z; + + public ParticleSpawnEvent(ParticleOptions options, double x, double y, double z) { + this.options = options; + this.x = x; + this.y = y; + this.z = z; + } + + public ParticleOptions options() { + return options; + } + + public double x() { + return x; + } + + public double y() { + return y; + } + + public double z() { + return z; + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/cosmetic/animation/CosmeticAttachmentTransforms.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/cosmetic/animation/CosmeticAttachmentTransforms.java deleted file mode 100644 index e10996f..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/cosmetic/animation/CosmeticAttachmentTransforms.java +++ /dev/null @@ -1,92 +0,0 @@ -package dev.loki.lovisual.features.cosmetic.animation; - -import com.mojang.blaze3d.vertex.PoseStack; -import com.mojang.math.Axis; -import dev.loki.lovisual.features.module.lifecycle.Modules; -import dev.loki.lovisual.features.module.modules.visuals.cosmetics.CosmeticDisplay; -import dev.loki.lovisual.features.cosmetic.model.CosmeticModel; -import dev.loki.lovisual.features.cosmetic.model.CosmeticSlot; - -public final class CosmeticAttachmentTransforms { - private CosmeticAttachmentTransforms() { - } - - /** - * Base attachment transform for a slot, then the user's per-slot offset/rotation/scale - * override from {@link CosmeticDisplay} (accessory slots only). - */ - public static void apply(PoseStack matrices, CosmeticSlot slot, CosmeticModel model) { - applyBase(matrices, slot); - if (slot.isAccessory()) { - applyUserOverride(matrices, slot); - } - } - - private static void applyUserOverride(PoseStack matrices, CosmeticSlot slot) { - CosmeticDisplay module = Modules.get(CosmeticDisplay.class); - if (module == null) return; - CosmeticDisplay.SlotOffsetSettings offsets = module.offsetsFor(slot); - if (offsets == null) return; - - matrices.translate(offsets.offsetX().get(), offsets.offsetY().get(), offsets.offsetZ().get()); - matrices.mulPose(Axis.YP.rotationDegrees(offsets.rotation().get())); - float scale = offsets.scale().get(); - matrices.scale(scale, scale, scale); - } - - private static void applyBase(PoseStack matrices, CosmeticSlot slot) { - switch (slot) { - case PET -> { - // TODO: replace with CosmeticAnimator.getPetHoverOffset() once the animator is ported. - float bob = 0f; - matrices.mulPose(Axis.YP.rotationDegrees(180f)); - matrices.translate(0.45f, 2.15f + bob, 0.12f); - matrices.scale(0.55f, 0.55f, 0.55f); - } - case HEAD -> { - matrices.translate(0f, 1.72f, 0f); - matrices.scale(0.52f, 0.52f, 0.52f); - } - case SHOULDERS -> { - matrices.translate(0f, 1.45f, 0f); - matrices.scale(0.5f, 0.5f, 0.5f); - } - case BACK -> { - matrices.translate(0f, 0.1f, -0.3f); - matrices.mulPose(Axis.YP.rotationDegrees(180f)); - matrices.scale(0.85f, 0.85f, 0.85f); - } - case WAIST -> { - matrices.translate(0f, 0.55f, 0.25f); - matrices.scale(0.55f, 0.55f, 0.55f); - } - case LEGS -> { - matrices.translate(0f, 0.3f, 0f); - matrices.scale(0.5f, 0.5f, 0.5f); - } - case HELD -> { - matrices.translate(0f, 1.3f, 0f); - matrices.scale(0.5f, 0.5f, 0.5f); - } - case MODEL -> { - // no base transform: full-body model replaces the player 1:1 - } - } - } - - public static void applyPreview(PoseStack matrices, CosmeticModel model) { - if (model == null) return; - CosmeticSlot slot = model.getSlot(); - if (slot == CosmeticSlot.PET) { - matrices.translate(6f, 22f, 1f); - matrices.scale(1.8f, 1.8f, 1.8f); - } else if (slot == CosmeticSlot.HEAD) { - matrices.translate(0f, 20f, 0f); - } else if (slot == CosmeticSlot.BACK) { - matrices.translate(0f, 2f, -4f); - matrices.scale(1.3f, 1.3f, 1.3f); - } else if (slot == CosmeticSlot.WAIST) { - matrices.translate(0f, 6f, 3f); - } - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/cosmetic/animation/CosmeticBbAnimation.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/cosmetic/animation/CosmeticBbAnimation.java deleted file mode 100644 index da823dc..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/cosmetic/animation/CosmeticBbAnimation.java +++ /dev/null @@ -1,225 +0,0 @@ -package dev.loki.lovisual.features.cosmetic.animation; - -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import org.joml.Vector3f; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import dev.loki.lovisual.features.cosmetic.model.CosmeticBone; -import dev.loki.lovisual.features.cosmetic.model.CosmeticModel; - -public final class CosmeticBbAnimation { - private final String name; - private final float length; - private final boolean loop; - private final Map tracksByBoneUuid = new java.util.HashMap<>(); - - public CosmeticBbAnimation(String name, float length, boolean loop) { - this.name = name; - this.length = Math.max(length, 1f / 20f); - this.loop = loop; - } - - public String getName() { - return name; - } - - public float getLength() { - return length; - } - - public boolean isLoop() { - return loop; - } - - public Map getTracks() { - return tracksByBoneUuid; - } - - public static Map parseAll(JsonObject bbmodel) { - Map map = new LinkedHashMap<>(); - if (!bbmodel.has("animations")) return map; - - for (JsonElement el : bbmodel.getAsJsonArray("animations")) { - if (!el.isJsonObject()) continue; - JsonObject obj = el.getAsJsonObject(); - String name = obj.has("name") ? obj.get("name").getAsString() : "anim"; - if (name.startsWith("--") || name.isBlank()) continue; - - float length = obj.has("length") ? obj.get("length").getAsFloat() : 1f; - boolean loop = !obj.has("loop") || obj.get("loop").getAsBoolean(); - CosmeticBbAnimation anim = new CosmeticBbAnimation(name, length, loop); - - if (obj.has("animators")) { - JsonObject animators = obj.getAsJsonObject("animators"); - for (Map.Entry entry : animators.entrySet()) { - if (!entry.getValue().isJsonObject()) continue; - JsonObject boneAnim = entry.getValue().getAsJsonObject(); - String boneUuid = entry.getKey(); - if (boneAnim.has("uuid")) { - boneUuid = boneAnim.get("uuid").getAsString(); - } - BoneTrack track = BoneTrack.parse(boneAnim); - if (!track.isEmpty()) { - anim.tracksByBoneUuid.put(boneUuid, track); - } - } - } - - if (anim.tracksByBoneUuid.isEmpty()) continue; - map.put(name.toLowerCase(), anim); - } - return map; - } - - public static void apply(CosmeticModel model, CosmeticBbAnimation animation, float time) { - java.util.Set filter = model.getAnimationBoneFilter(); - for (Map.Entry entry : animation.tracksByBoneUuid.entrySet()) { - if (filter != null && !filter.contains(entry.getKey())) continue; - CosmeticBone bone = model.getBoneByUuid(entry.getKey()); - if (bone == null) continue; - - BoneTrack track = entry.getValue(); - Vector3f rot = track.sampleRotation(time, animation.length, animation.loop); - Vector3f pos = track.samplePosition(time, animation.length, animation.loop); - - if (rot != null) { - bone.setAnimRotation(new Vector3f( - rot.x - bone.getDefaultRotation().x, - rot.y - bone.getDefaultRotation().y, - rot.z - bone.getDefaultRotation().z - )); - } - if (pos != null) { - bone.setAnimPosition(pos); - } - } - } - - public static final class BoneTrack { - private final List rotations = new ArrayList<>(); - private final List positions = new ArrayList<>(); - - static BoneTrack parse(JsonObject boneAnim) { - BoneTrack track = new BoneTrack(); - if (!boneAnim.has("keyframes")) return track; - - for (JsonElement el : boneAnim.getAsJsonArray("keyframes")) { - if (!el.isJsonObject()) continue; - JsonObject kf = el.getAsJsonObject(); - String channel = kf.has("channel") ? kf.get("channel").getAsString() : ""; - float t = kf.has("time") ? kf.get("time").getAsFloat() : 0f; - Vector3f value = readPoint(kf); - String interp = kf.has("interpolation") ? kf.get("interpolation").getAsString() : "linear"; - Keyframe keyframe = new Keyframe(t, value, interp); - - switch (channel) { - case "rotation" -> track.rotations.add(keyframe); - case "position" -> track.positions.add(keyframe); - default -> { - } - } - } - - track.rotations.sort(Comparator.comparing(Keyframe::time)); - track.positions.sort(Comparator.comparing(Keyframe::time)); - return track; - } - - boolean isEmpty() { - return rotations.isEmpty() && positions.isEmpty(); - } - - Vector3f sampleRotation(float time, float length, boolean loop) { - return sample(rotations, time, length, loop); - } - - Vector3f samplePosition(float time, float length, boolean loop) { - return sample(positions, time, length, loop); - } - - private static Vector3f sample(List frames, float time, float length, boolean loop) { - if (frames.isEmpty()) return null; - if (frames.size() == 1) return new Vector3f(frames.get(0).value); - - float t = loop ? wrapTime(time, length) : Math.min(time, length); - Keyframe prev = frames.get(0); - Keyframe next = frames.get(frames.size() - 1); - - for (int i = 0; i < frames.size() - 1; i++) { - Keyframe a = frames.get(i); - Keyframe b = frames.get(i + 1); - if (t >= a.time && t <= b.time) { - prev = a; - next = b; - break; - } - } - if (t >= frames.get(frames.size() - 1).time) { - prev = frames.get(frames.size() - 1); - next = prev; - } - - if (prev == next || Math.abs(next.time - prev.time) < 0.0001f) { - return new Vector3f(prev.value); - } - - float alpha = (t - prev.time) / (next.time - prev.time); - if ("step".equals(prev.interpolation)) { - return new Vector3f(prev.value); - } - return lerp(prev.value, next.value, clamp01(alpha)); - } - - private static float wrapTime(float time, float length) { - if (length <= 0f) return time; - float wrapped = time % length; - return wrapped < 0f ? wrapped + length : wrapped; - } - - private static float clamp01(float v) { - return Math.max(0f, Math.min(1f, v)); - } - - private static Vector3f lerp(Vector3f a, Vector3f b, float t) { - return new Vector3f( - a.x + (b.x - a.x) * t, - a.y + (b.y - a.y) * t, - a.z + (b.z - a.z) * t - ); - } - - private static Vector3f readPoint(JsonObject kf) { - if (!kf.has("data_points")) return new Vector3f(); - JsonArray points = kf.getAsJsonArray("data_points"); - if (points.isEmpty()) return new Vector3f(); - JsonObject point = points.get(0).getAsJsonObject(); - return new Vector3f( - parseNum(point, "x"), - parseNum(point, "y"), - parseNum(point, "z") - ); - } - - private static float parseNum(JsonObject obj, String key) { - if (!obj.has(key)) return 0f; - JsonElement el = obj.get(key); - if (el.isJsonPrimitive() && el.getAsJsonPrimitive().isNumber()) { - return el.getAsFloat(); - } - try { - return Float.parseFloat(el.getAsString().trim()); - } catch (Exception ignored) { - return 0f; - } - } - } - - private record Keyframe(float time, Vector3f value, String interpolation) { - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/cosmetic/animation/CosmeticTextureSlot.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/cosmetic/animation/CosmeticTextureSlot.java deleted file mode 100644 index 848d4e2..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/cosmetic/animation/CosmeticTextureSlot.java +++ /dev/null @@ -1,124 +0,0 @@ -package dev.loki.lovisual.features.cosmetic.animation; - -import com.google.gson.JsonObject; -import com.mojang.blaze3d.platform.NativeImage; -import net.minecraft.client.Minecraft; -import net.minecraft.client.renderer.texture.DynamicTexture; -import net.minecraft.resources.Identifier; - -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.Base64; - -public final class CosmeticTextureSlot { - private final int index; - private final Identifier id; - private int width; - private int height; - private final String fileName; - - private boolean loaded; - private JsonObject textureJson; - - public CosmeticTextureSlot(int index, Identifier id, int width, int height, String fileName) { - - this.index = index; - this.id = id; - this.width = Math.max(width, 1); - this.height = Math.max(height, 1); - this.fileName = fileName; - } - - public int getIndex() { - return index; - } - - public Identifier getId() { - return id; - } - - public int getWidth() { - return width; - } - - public int getHeight() { - return height; - } - - public boolean isLoaded() { - return loaded; - } - - public void markLoaded() { - - loaded = true; - } - - public static CosmeticTextureSlot fromJson(int index, String modelKey, JsonObject texture) { - - int w = texture.has("width") ? texture.get("width").getAsInt() : 64; - int h = texture.has("height") ? texture.get("height").getAsInt() : 64; - String name = texture.has("name") ? texture.get("name").getAsString() : "tex" + index; - Identifier id = Identifier.fromNamespaceAndPath( - "lovisual", - "dynamic/cosmetic/" + modelKey + "/tex/" + index - ); - CosmeticTextureSlot slot = new CosmeticTextureSlot(index, id, w, h, name); - slot.textureJson = texture; - return slot; - } - - public void reset() { - - loaded = false; - } - - /** - * Attempts to load this texture slot from, in order: the cosmetic's external folder - * (file name, then outfits/<file>, then a generic texture.png fallback), or an - * inline base64 "source" embedded in the .bbmodel JSON. - */ - public void load(Minecraft mc, File externalDir, String resourcePath) { - - if (loaded || mc == null || mc.getTextureManager() == null) return; - - try { - if (externalDir != null) { - if (tryFile(mc, new File(externalDir, fileName))) return; - if (tryFile(mc, new File(externalDir, "outfits/" + fileName))) return; - if (tryFile(mc, new File(externalDir, "texture.png"))) return; - } - - if (textureJson != null && textureJson.has("source")) { - String source = textureJson.get("source").getAsString(); - if (source.startsWith("data:image") && source.contains(",")) { - byte[] bytes = Base64.getDecoder().decode(source.substring(source.indexOf(',') + 1)); - try (InputStream is = new ByteArrayInputStream(bytes)) { - register(mc, is); - } - } - } - } catch (Exception ignored) { - } - } - - private boolean tryFile(Minecraft mc, File file) throws IOException { - if (!file.exists()) return false; - try (InputStream is = new FileInputStream(file)) { - register(mc, is); - return true; - } - } - - private void register(Minecraft mc, InputStream is) throws IOException { - NativeImage image = NativeImage.read(is); - width = Math.max(image.getWidth(), 1); - height = Math.max(image.getHeight(), 1); - DynamicTexture texture = new DynamicTexture(() -> "lovisual_cosmetic_" + index, image); - mc.getTextureManager().register(id, texture); - loaded = true; - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/cosmetic/animation/CosmeticType.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/cosmetic/animation/CosmeticType.java deleted file mode 100644 index 01a8c18..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/cosmetic/animation/CosmeticType.java +++ /dev/null @@ -1,32 +0,0 @@ -package dev.loki.lovisual.features.cosmetic.animation; - -public enum CosmeticType { - MODEL("model", "Модель"), - PET("pet", "Питомец"), - ACCESSORY("accessory", "Аксессуар"); - - private final String id; - private final String displayName; - - CosmeticType(String id, String displayName) { - this.id = id; - this.displayName = displayName; - } - - public String getId() { - return id; - } - - public String getDisplayName() { - return displayName; - } - - public static CosmeticType fromJson(String raw) { - if (raw == null || raw.isBlank()) return MODEL; - return switch (raw.toLowerCase()) { - case "pet", "pets", "питомец", "питомцы" -> PET; - case "accessory", "accessories", "item", "items", "аксессуар", "аксессуары" -> ACCESSORY; - default -> MODEL; - }; - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/cosmetic/manager/CosmeticManager.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/cosmetic/manager/CosmeticManager.java deleted file mode 100644 index 1f8c2be..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/cosmetic/manager/CosmeticManager.java +++ /dev/null @@ -1,83 +0,0 @@ -package dev.loki.lovisual.features.cosmetic.manager; - -import java.util.EnumMap; -import java.util.Map; -import dev.loki.lovisual.features.cosmetic.manager.CosmeticRepository; -import dev.loki.lovisual.features.cosmetic.model.CosmeticModel; -import dev.loki.lovisual.features.cosmetic.model.CosmeticSlot; - -/** - * Holds which cosmetic the local user currently has equipped, per slot. - * MODEL/PET are single-active slots; the six accessory slots (HEAD/SHOULDERS/ - * BACK/WAIST/LEGS/HELD) are independent and can all be active at once. - * Rendering and animation live in separate classes (not part of this core pass). - */ -public final class CosmeticManager { - private static final CosmeticManager INSTANCE = new CosmeticManager(); - - private final Map active = new EnumMap<>(CosmeticSlot.class); - - private CosmeticManager() { - } - - public static CosmeticManager get() { - return INSTANCE; - } - - /** - * Reloads the catalog from disk, re-resolving the currently equipped cosmetics - * (by id) against the freshly loaded models. - */ - public void init() { - Map equippedIds = new EnumMap<>(CosmeticSlot.class); - for (Map.Entry entry : active.entrySet()) { - if (entry.getValue() != null) { - equippedIds.put(entry.getKey(), entry.getValue().getId()); - } - } - - for (CosmeticModel model : CosmeticRepository.get().all()) { - model.resetTexture(); - } - CosmeticRepository.get().reload(); - - active.clear(); - CosmeticRepository repo = CosmeticRepository.get(); - for (Map.Entry entry : equippedIds.entrySet()) { - CosmeticModel resolved = repo.get(entry.getValue()); - if (resolved != null) { - active.put(entry.getKey(), resolved); - } - } - } - - public CosmeticModel getActive(CosmeticSlot slot) { - return active.get(slot); - } - - public void setActive(CosmeticSlot slot, CosmeticModel model) { - if (model != null) { - CosmeticModel resolved = CosmeticRepository.get().get(model.getId()); - if (resolved != null) { - model = resolved; - } - } - if (model == null) { - active.remove(slot); - } else { - active.put(slot, model); - } - } - - public void clear(CosmeticSlot slot) { - setActive(slot, null); - } - - public boolean hasAnyActive() { - return !active.isEmpty(); - } - - public boolean isActive() { - return active.get(CosmeticSlot.MODEL) != null; - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/cosmetic/manager/CosmeticRenderer.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/cosmetic/manager/CosmeticRenderer.java deleted file mode 100644 index 6919da4..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/cosmetic/manager/CosmeticRenderer.java +++ /dev/null @@ -1,211 +0,0 @@ -package dev.loki.lovisual.features.cosmetic.manager; - -import com.mojang.blaze3d.vertex.PoseStack; -import com.mojang.math.Axis; -import dev.loki.lovisual.render.engine.RenderState; -import dev.loki.lovisual.render.engine.math.RenderMath; -import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; -import dev.loki.lovisual.render.engine.renderer.Renderer3D; -import dev.loki.lovisual.render.engine.uniform.MeshBuilder; -import net.minecraft.client.Minecraft; -import net.minecraft.client.renderer.texture.AbstractTexture; -import net.minecraft.util.Mth; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.phys.Vec3; -import org.joml.Matrix4f; -import org.joml.Vector3f; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import dev.loki.lovisual.features.cosmetic.animation.CosmeticAttachmentTransforms; -import dev.loki.lovisual.features.cosmetic.animation.CosmeticTextureSlot; -import dev.loki.lovisual.features.cosmetic.manager.CosmeticManager; -import dev.loki.lovisual.features.cosmetic.model.CosmeticBone; -import dev.loki.lovisual.features.cosmetic.model.CosmeticCube; -import dev.loki.lovisual.features.cosmetic.model.CosmeticModel; -import dev.loki.lovisual.features.cosmetic.model.CosmeticSlot; - -/** - * Renders a rest-pose (no bb-animation) {@link CosmeticModel} bone/cube tree attached to a - * player, as camera-relative textured quads through the engine's {@link Renderer3D}/{@link MeshBuilder}. - */ -public final class CosmeticRenderer { - private static final float UNITS_PER_BLOCK = 16.0f; - - private CosmeticRenderer() { - } - - public static void renderOnPlayer(Renderer3D renderer, Player player, CosmeticSlot slot, float tickDelta) { - if (renderer == null || player == null) return; - - CosmeticModel model = CosmeticManager.get().getActive(slot); - if (model == null) return; - - model.loadTexture(); - List roots = model.getRenderRoots(); - if (roots.isEmpty()) return; - - Vec3 pos = RenderMath.getLerpedPos(player, tickDelta); - float bodyYaw = Mth.rotLerp(tickDelta, player.yBodyRotO, player.yBodyRot); - - PoseStack matrices = new PoseStack(); - matrices.translate(pos.x, pos.y, pos.z); - CosmeticAttachmentTransforms.apply(matrices, slot, model); - matrices.mulPose(Axis.YP.rotationDegrees(180f - bodyYaw)); - - Map> byTexture = new HashMap<>(); - for (CosmeticBone bone : roots) { - collectBone(matrices, model, bone, byTexture); - } - if (byTexture.isEmpty()) return; - - Minecraft mc = Minecraft.getInstance(); - Vec3 cam = RenderState.cameraPos; - - for (Map.Entry> entry : byTexture.entrySet()) { - CosmeticTextureSlot textureSlot = model.getTextureSlot(entry.getKey()); - if (textureSlot == null) continue; - if (!textureSlot.isLoaded()) { - model.ensureTextureSlotLoaded(textureSlot); - } - if (!textureSlot.isLoaded()) continue; - - AbstractTexture texture = mc.getTextureManager().getTexture(textureSlot.getId()); - if (texture == null || texture.getTextureView() == null || texture.getSampler() == null) continue; - - Renderer3D.BatchBindings bindings = Renderer3D.BatchBindings.none() - .withSampler("u_Texture", texture.getTextureView(), texture.getSampler()); - MeshBuilder mesh = renderer.batch(LoVisualRenderPipelines.WORLD_TEXTURED, Renderer3D.DepthMode.MAIN, bindings); - if (mesh == null) continue; - - for (PendingFace face : entry.getValue()) { - mesh.ensureQuadCapacity(); - - int i1 = mesh.vec3(face.xs[0] - cam.x, face.ys[0] - cam.y, face.zs[0] - cam.z) - .raw2(face.us[0], face.vs[0]).color(255, 255, 255, 255).next(); - int i2 = mesh.vec3(face.xs[1] - cam.x, face.ys[1] - cam.y, face.zs[1] - cam.z) - .raw2(face.us[1], face.vs[1]).color(255, 255, 255, 255).next(); - int i3 = mesh.vec3(face.xs[2] - cam.x, face.ys[2] - cam.y, face.zs[2] - cam.z) - .raw2(face.us[2], face.vs[2]).color(255, 255, 255, 255).next(); - int i4 = mesh.vec3(face.xs[3] - cam.x, face.ys[3] - cam.y, face.zs[3] - cam.z) - .raw2(face.us[3], face.vs[3]).color(255, 255, 255, 255).next(); - - mesh.quad(i1, i2, i3, i4); - } - } - } - - private static void collectBone(PoseStack matrices, CosmeticModel model, CosmeticBone bone, - Map> out) { - if (model.shouldSkipBone(bone)) return; - - matrices.pushPose(); - matrices.last().pose().mul(bone.buildMatrix()); - - for (CosmeticCube cube : bone.getCubes()) { - collectCube(matrices, cube, out); - } - for (CosmeticBone child : bone.getChildren()) { - collectBone(matrices, model, child, out); - } - - matrices.popPose(); - } - - private static void collectCube(PoseStack matrices, CosmeticCube cube, Map> out) { - matrices.pushPose(); - - Vector3f origin = cube.getOrigin(); - Vector3f rotation = cube.getRotation(); - if (rotation.x != 0f || rotation.y != 0f || rotation.z != 0f) { - matrices.translate(origin.x / UNITS_PER_BLOCK, origin.y / UNITS_PER_BLOCK, origin.z / UNITS_PER_BLOCK); - matrices.mulPose(Axis.XP.rotationDegrees(rotation.x)); - matrices.mulPose(Axis.YP.rotationDegrees(rotation.y)); - matrices.mulPose(Axis.ZP.rotationDegrees(rotation.z)); - matrices.translate(-origin.x / UNITS_PER_BLOCK, -origin.y / UNITS_PER_BLOCK, -origin.z / UNITS_PER_BLOCK); - } - - float inflate = cube.getInflate(); - float x1 = (cube.getFrom().x - inflate) / UNITS_PER_BLOCK; - float y1 = (cube.getFrom().y - inflate) / UNITS_PER_BLOCK; - float z1 = (cube.getFrom().z - inflate) / UNITS_PER_BLOCK; - float x2 = (cube.getTo().x + inflate) / UNITS_PER_BLOCK; - float y2 = (cube.getTo().y + inflate) / UNITS_PER_BLOCK; - float z2 = (cube.getTo().z + inflate) / UNITS_PER_BLOCK; - - Matrix4f m = new Matrix4f(matrices.last().pose()); - CosmeticCube.CubeFace[] faces = cube.getFaces(); - int tw = cube.getTextureWidth(); - int th = cube.getTextureHeight(); - - for (int side = 0; side < 6; side++) { - CosmeticCube.CubeFace face = faces[side]; - if (face == null) continue; - addFace(m, face, tw, th, x1, y1, z1, x2, y2, z2, side, out); - } - - matrices.popPose(); - } - - private static void addFace(Matrix4f m, CosmeticCube.CubeFace face, float tw, float th, - float x1, float y1, float z1, float x2, float y2, float z2, - int side, Map> out) { - PendingFace quad = switch (side) { - case 0 -> quad(m, - x2, y2, z1, x1, y2, z1, x1, y1, z1, x2, y1, z1); - case 1 -> quad(m, - x2, y2, z2, x2, y2, z1, x2, y1, z1, x2, y1, z2); - case 2 -> quad(m, - x1, y2, z2, x2, y2, z2, x2, y1, z2, x1, y1, z2); - case 3 -> quad(m, - x1, y2, z1, x1, y2, z2, x1, y1, z2, x1, y1, z1); - case 4 -> quad(m, - x1, y2, z2, x2, y2, z2, x2, y2, z1, x1, y2, z1); - case 5 -> quad(m, - x1, y1, z1, x2, y1, z1, x2, y1, z2, x1, y1, z2); - default -> null; - }; - if (quad == null) return; - - float u1 = face.getU1() / tw; - float v1 = face.getV1() / th; - float u2 = face.getU2() / tw; - float v2 = face.getV2() / th; - quad.us[0] = u1; - quad.vs[0] = v1; - quad.us[1] = u2; - quad.vs[1] = v1; - quad.us[2] = u2; - quad.vs[2] = v2; - quad.us[3] = u1; - quad.vs[3] = v2; - - out.computeIfAbsent(face.getTextureIndex(), k -> new ArrayList<>()).add(quad); - } - - private static PendingFace quad(Matrix4f m, - float x1, float y1, float z1, - float x2, float y2, float z2, - float x3, float y3, float z3, - float x4, float y4, float z4) { - PendingFace face = new PendingFace(); - Vector3f p1 = m.transformPosition(new Vector3f(x1, y1, z1)); - Vector3f p2 = m.transformPosition(new Vector3f(x2, y2, z2)); - Vector3f p3 = m.transformPosition(new Vector3f(x3, y3, z3)); - Vector3f p4 = m.transformPosition(new Vector3f(x4, y4, z4)); - face.xs = new float[]{p1.x, p2.x, p3.x, p4.x}; - face.ys = new float[]{p1.y, p2.y, p3.y, p4.y}; - face.zs = new float[]{p1.z, p2.z, p3.z, p4.z}; - return face; - } - - private static final class PendingFace { - float[] xs; - float[] ys; - float[] zs; - final float[] us = new float[4]; - final float[] vs = new float[4]; - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/cosmetic/manager/CosmeticRepository.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/cosmetic/manager/CosmeticRepository.java deleted file mode 100644 index 5b981a0..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/cosmetic/manager/CosmeticRepository.java +++ /dev/null @@ -1,104 +0,0 @@ -package dev.loki.lovisual.features.cosmetic.manager; - -import dev.loki.lovisual.config.io.ConfigPaths; - -import java.io.File; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Stream; -import dev.loki.lovisual.features.cosmetic.animation.CosmeticType; -import dev.loki.lovisual.features.cosmetic.model.CosmeticModel; -import dev.loki.lovisual.features.cosmetic.model.CosmeticSlot; - -/** - * Scans {@code config/lovisual/cosmetics/<type>/<name>} (and the flat root as a - * fallback) for user-provided cosmetic folders (avatar.json + .bbmodel) and keeps the - * loaded catalog in memory. - */ -public final class CosmeticRepository { - private static final CosmeticRepository INSTANCE = new CosmeticRepository(); - - private final Map catalog = new LinkedHashMap<>(); - - public static CosmeticRepository get() { - return INSTANCE; - } - - public Path root() { - return ConfigPaths.root().resolve("cosmetics"); - } - - public void reload() { - catalog.clear(); - discoverUserFolder(root()); - } - - public List all() { - return new ArrayList<>(catalog.values()); - } - - public List byType(CosmeticType type) { - return catalog.values().stream().filter(m -> m.getType() == type).toList(); - } - - public List bySlot(CosmeticSlot slot) { - return catalog.values().stream().filter(m -> m.getSlot() == slot).toList(); - } - - public CosmeticModel get(String id) { - return catalog.get(id); - } - - private void register(CosmeticModel model) { - if (model != null && !catalog.containsKey(model.getId())) { - catalog.put(model.getId(), model); - } - } - - private void discoverUserFolder(Path root) { - if (!Files.isDirectory(root)) { - try { - Files.createDirectories(root); - for (CosmeticType type : CosmeticType.values()) { - Files.createDirectories(root.resolve(type.getId())); - } - } catch (Exception ignored) { - } - return; - } - - for (CosmeticType type : CosmeticType.values()) { - Path typeDir = root.resolve(type.getId()); - scanDirectory(typeDir, type); - } - scanDirectory(root, null); - } - - private void scanDirectory(Path dir, CosmeticType forcedType) { - if (!Files.isDirectory(dir)) return; - try (Stream stream = Files.list(dir)) { - stream.filter(Files::isDirectory).forEach(sub -> { - File folder = sub.toFile(); - if (new File(folder, "avatar.json").exists() || hasBbmodel(folder)) { - CosmeticModel model = CosmeticModel.loadExternal(folder, "user"); - if (model != null) { - if (forcedType != null && model.getType() == CosmeticType.MODEL) { - model = model.withType(forcedType); - } - register(model); - } - } - }); - } catch (Exception ignored) { - } - } - - private static boolean hasBbmodel(File folder) { - return new File(folder, "model.bbmodel").exists() - || new File(folder, folder.getName() + ".bbmodel").exists(); - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/cosmetic/model/CosmeticBone.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/cosmetic/model/CosmeticBone.java deleted file mode 100644 index 99866be..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/cosmetic/model/CosmeticBone.java +++ /dev/null @@ -1,67 +0,0 @@ -package dev.loki.lovisual.features.cosmetic.model; - -import lombok.Getter; -import lombok.Setter; -import org.joml.Matrix4f; -import org.joml.Vector3f; - -import java.util.ArrayList; -import java.util.List; -import dev.loki.lovisual.features.cosmetic.model.CosmeticCube; - -@Getter -public class CosmeticBone { - private final String name; - private final String uuid; - private final Vector3f pivot; - private final Vector3f defaultRotation; - private final List cubes = new ArrayList<>(); - private final List children = new ArrayList<>(); - - @Setter private Vector3f animRotation = new Vector3f(0, 0, 0); - @Setter private Vector3f animPosition = new Vector3f(0, 0, 0); - - public CosmeticBone(String name, String uuid, Vector3f pivot, Vector3f defaultRotation) { - this.name = name; - this.uuid = uuid; - this.pivot = pivot; - this.defaultRotation = defaultRotation; - } - - public Matrix4f buildMatrix() { - Matrix4f mat = new Matrix4f(); - - mat.translate(pivot.x / 16f, pivot.y / 16f, pivot.z / 16f); - - if (defaultRotation.x != 0 || defaultRotation.y != 0 || defaultRotation.z != 0 - || animRotation.x != 0 || animRotation.y != 0 || animRotation.z != 0) { - float rx = (float) Math.toRadians(defaultRotation.x + animRotation.x); - float ry = (float) Math.toRadians(defaultRotation.y + animRotation.y); - float rz = (float) Math.toRadians(defaultRotation.z + animRotation.z); - mat.rotateZYX(rz, ry, rx); - } - - mat.translate(-pivot.x / 16f, -pivot.y / 16f, -pivot.z / 16f); - - if (animPosition.x != 0 || animPosition.y != 0 || animPosition.z != 0) { - mat.translate(animPosition.x / 16f, animPosition.y / 16f, animPosition.z / 16f); - } - - return mat; - } - - public void resetAnim() { - animRotation = new Vector3f(0, 0, 0); - animPosition = new Vector3f(0, 0, 0); - for (CosmeticBone child : children) child.resetAnim(); - } - - public CosmeticBone deepClone() { - CosmeticBone copy = new CosmeticBone(name, uuid, new Vector3f(pivot), new Vector3f(defaultRotation)); - copy.getCubes().addAll(cubes); - for (CosmeticBone child : children) { - copy.getChildren().add(child.deepClone()); - } - return copy; - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/cosmetic/model/CosmeticCube.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/cosmetic/model/CosmeticCube.java deleted file mode 100644 index ba94d3b..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/cosmetic/model/CosmeticCube.java +++ /dev/null @@ -1,42 +0,0 @@ -package dev.loki.lovisual.features.cosmetic.model; - -import lombok.Getter; -import org.joml.Vector3f; - -@Getter -public class CosmeticCube { - private final Vector3f from; - private final Vector3f to; - private final Vector3f rotation; - private final Vector3f origin; - private final float inflate; - private final CubeFace[] faces; - private final int textureWidth; - private final int textureHeight; - - public CosmeticCube(Vector3f from, Vector3f to, Vector3f rotation, Vector3f origin, - float inflate, CubeFace[] faces, int textureWidth, int textureHeight) { - this.from = from; - this.to = to; - this.rotation = rotation; - this.origin = origin; - this.inflate = inflate; - this.faces = faces; - this.textureWidth = textureWidth; - this.textureHeight = textureHeight; - } - - @Getter - public static class CubeFace { - private final float u1, v1, u2, v2; - private final int textureIndex; - - public CubeFace(float u1, float v1, float u2, float v2, int textureIndex) { - this.u1 = u1; - this.v1 = v1; - this.u2 = u2; - this.v2 = v2; - this.textureIndex = textureIndex; - } - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/cosmetic/model/CosmeticModel.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/cosmetic/model/CosmeticModel.java deleted file mode 100644 index dfd4724..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/cosmetic/model/CosmeticModel.java +++ /dev/null @@ -1,677 +0,0 @@ -package dev.loki.lovisual.features.cosmetic.model; - -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.mojang.blaze3d.platform.NativeImage; -import lombok.Getter; -import lombok.Setter; -import net.minecraft.client.Minecraft; -import net.minecraft.client.renderer.texture.DynamicTexture; -import net.minecraft.resources.Identifier; -import org.joml.Vector3f; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; -import dev.loki.lovisual.features.cosmetic.animation.CosmeticBbAnimation; -import dev.loki.lovisual.features.cosmetic.animation.CosmeticTextureSlot; -import dev.loki.lovisual.features.cosmetic.animation.CosmeticType; -import dev.loki.lovisual.features.cosmetic.model.CosmeticBone; -import dev.loki.lovisual.features.cosmetic.model.CosmeticCube; -import dev.loki.lovisual.features.cosmetic.model.CosmeticSlot; - -/** - * A single cosmetic asset: a Blockbench (.bbmodel) bone/cube tree plus its texture(s), - * loaded from a user-provided folder (avatar.json + model.bbmodel + textures). - */ -@Getter -public class CosmeticModel { - private final String id; - private final String name; - private final String description; - private final String accentColor; - private final String folderName; - private final CosmeticType type; - private final CosmeticSlot slot; - private final File externalDirectory; - @Setter private String source = "Своя"; - - private final List rootBones = new ArrayList<>(); - private final List renderRoots = new ArrayList<>(); - private boolean customLegsPresent; - private boolean bowReplacementAccessory; - private boolean swordReplacementAccessory; - private boolean hatOnlyAccessory; - private boolean wingsOnlyAccessory; - private Set visibleBoneUuids; - @Setter private Set animationBoneFilter; - private final Map boneMap = new HashMap<>(); - private final Map boneMapByUuid = new HashMap<>(); - private final Map bbAnimations = new LinkedHashMap<>(); - private final List textureSlots = new ArrayList<>(); - private int textureWidth = 64; - private int textureHeight = 64; - private Identifier textureId; - private boolean textureLoaded = false; - private CosmeticTextureSlot fallbackSlot; - - private CosmeticModel(String id, String name, String description, String accentColor, - String folderName, CosmeticType type, CosmeticSlot slot, File externalDirectory) { - this.id = id; - this.name = name; - this.description = description; - this.accentColor = accentColor; - this.folderName = folderName; - this.type = type; - this.slot = slot != null ? slot : defaultSlotFor(type, folderName); - this.externalDirectory = externalDirectory; - } - - private static CosmeticSlot defaultSlotFor(CosmeticType type, String folderName) { - return switch (type) { - case MODEL -> CosmeticSlot.MODEL; - case PET -> CosmeticSlot.PET; - case ACCESSORY -> CosmeticSlot.guessFromFolderName(folderName); - }; - } - - public CosmeticModel withType(CosmeticType newType) { - CosmeticModel copy = new CosmeticModel(id, name, description, accentColor, folderName, newType, null, externalDirectory); - copy.rootBones.addAll(rootBones); - copy.renderRoots.addAll(renderRoots); - copy.boneMap.putAll(boneMap); - copy.boneMapByUuid.putAll(boneMapByUuid); - copy.bbAnimations.putAll(bbAnimations); - copy.textureSlots.addAll(textureSlots); - copy.textureWidth = textureWidth; - copy.textureHeight = textureHeight; - copy.textureId = textureId; - copy.textureLoaded = textureLoaded; - copy.source = source; - copy.postProcess(); - return copy; - } - - public void resetTexture() { - textureLoaded = false; - textureId = null; - for (CosmeticTextureSlot slot : textureSlots) { - slot.reset(); - } - } - - public CosmeticTextureSlot getTextureSlot(int index) { - if (!textureSlots.isEmpty()) { - if (index >= 0 && index < textureSlots.size()) { - return textureSlots.get(index); - } - return textureSlots.get(0); - } - if (textureId == null) return null; - if (fallbackSlot == null) { - fallbackSlot = new CosmeticTextureSlot(0, textureId, textureWidth, textureHeight, "fallback"); - if (textureLoaded) fallbackSlot.markLoaded(); - } - return fallbackSlot; - } - - /** - * Loads a cosmetic from a user folder: {@code avatar.json} (metadata) plus a - * {@code .bbmodel} (geometry/animation) and its texture(s). - */ - public static CosmeticModel loadExternal(File folder, String sourceTag) { - if (folder == null || !folder.isDirectory()) return null; - String key = sourceTag + ":" + folder.getName().toLowerCase(); - return load(folder.getName(), folder, key, null); - } - - private static CosmeticModel load(String folderName, File externalDir, String catalogId, CosmeticType forcedType) { - try { - JsonObject avatar = new JsonObject(); - File avatarFile = new File(externalDir, "avatar.json"); - if (avatarFile.exists()) { - try (InputStreamReader reader = new InputStreamReader(new FileInputStream(avatarFile), StandardCharsets.UTF_8)) { - avatar = JsonParser.parseReader(reader).getAsJsonObject(); - } - } - - String name = avatar.has("name") ? avatar.get("name").getAsString() : folderName; - String desc = avatar.has("description") ? avatar.get("description").getAsString() : ""; - String color = avatar.has("color") ? avatar.get("color").getAsString() : "#ffffff"; - CosmeticType type = forcedType != null ? forcedType : CosmeticType.fromJson( - avatar.has("category") ? avatar.get("category").getAsString() - : avatar.has("type") ? avatar.get("type").getAsString() : null - ); - - CosmeticSlot slot = avatar.has("slot") ? CosmeticSlot.fromJson(avatar.get("slot").getAsString()) : null; - CosmeticModel model = new CosmeticModel(catalogId, name, desc, color, folderName, type, slot, externalDir); - - InputStream modelStream = openModelStream(externalDir, folderName); - if (modelStream == null) { - return model; - } - - JsonObject bbmodel; - try (InputStreamReader reader = new InputStreamReader(modelStream, StandardCharsets.UTF_8)) { - bbmodel = JsonParser.parseReader(reader).getAsJsonObject(); - } - - if (bbmodel.has("resolution")) { - JsonObject res = bbmodel.getAsJsonObject("resolution"); - model.textureWidth = res.get("width").getAsInt(); - model.textureHeight = res.get("height").getAsInt(); - } - - model.textureId = Identifier.fromNamespaceAndPath( - "lovisual", - "dynamic/cosmetic/" + catalogId.replace(':', '_').replace(' ', '_') - ); - model.loadTextureSlots(bbmodel); - - Map elementMap = new HashMap<>(); - if (bbmodel.has("elements")) { - for (JsonElement el : bbmodel.getAsJsonArray("elements")) { - JsonObject obj = el.getAsJsonObject(); - if (obj.has("uuid")) { - elementMap.put(obj.get("uuid").getAsString(), obj); - } - } - } - - Map groupMap = buildGroupMap(bbmodel); - - if (bbmodel.has("outliner")) { - for (JsonElement el : bbmodel.getAsJsonArray("outliner")) { - CosmeticBone bone = parseOutliner(el, elementMap, groupMap, model.textureWidth, model.textureHeight); - if (bone != null) { - model.rootBones.add(bone); - model.indexBone(bone); - } - } - } - - model.bbAnimations.putAll(CosmeticBbAnimation.parseAll(bbmodel)); - model.postProcess(); - - return model; - } catch (Exception e) { - return null; - } - } - - private static InputStream openModelStream(File externalDir, String folderName) throws IOException { - File model = new File(externalDir, "model.bbmodel"); - if (!model.exists()) { - model = new File(externalDir, folderName + ".bbmodel"); - } - if (model.exists()) return new FileInputStream(model); - File[] bbmodels = externalDir.listFiles((dir, name) -> name.endsWith(".bbmodel")); - if (bbmodels != null && bbmodels.length > 0) { - return new FileInputStream(bbmodels[0]); - } - return null; - } - - private void loadTextureSlots(JsonObject bbmodel) { - textureSlots.clear(); - if (!bbmodel.has("textures")) return; - - String modelKey = id.replace(':', '_'); - JsonArray textures = bbmodel.getAsJsonArray("textures"); - for (int i = 0; i < textures.size(); i++) { - if (!textures.get(i).isJsonObject()) continue; - JsonObject tex = textures.get(i).getAsJsonObject(); - textureSlots.add(CosmeticTextureSlot.fromJson(i, modelKey, tex)); - } - } - - public void ensureTextureSlotLoaded(CosmeticTextureSlot slot) { - if (slot == null || slot.isLoaded()) return; - Minecraft mc = Minecraft.getInstance(); - if (mc == null) return; - slot.load(mc, externalDirectory, folderName); - } - - /** - * Loads whichever textures this model needs (per-slot bbmodel textures, or a single - * fallback {@code texture.png}/{@code avatar.png} in the cosmetic's folder). - */ - public void loadTexture() { - if (textureLoaded) return; - try { - Minecraft mc = Minecraft.getInstance(); - if (mc == null || mc.getTextureManager() == null) return; - - boolean anyLoaded = false; - for (CosmeticTextureSlot slot : textureSlots) { - slot.load(mc, externalDirectory, folderName); - if (slot.isLoaded()) anyLoaded = true; - } - - if (!anyLoaded && externalDirectory != null) { - String[] names = {"outfits/texture.png", "texture.png", folderName + ".png", "avatar.png"}; - for (String rel : names) { - File tex = new File(externalDirectory, rel); - if (!tex.exists()) continue; - try (InputStream is = new FileInputStream(tex)) { - registerTexture(mc, textureId, is); - anyLoaded = true; - break; - } catch (Exception ignored) { - } - } - } else if (!textureSlots.isEmpty() && textureSlots.get(0).isLoaded()) { - textureId = textureSlots.get(0).getId(); - textureWidth = textureSlots.get(0).getWidth(); - textureHeight = textureSlots.get(0).getHeight(); - } - - textureLoaded = anyLoaded; - if (!anyLoaded) { - textureId = null; - } - } catch (Exception ignored) { - } - } - - private void registerTexture(Minecraft mc, Identifier rawId, InputStream is) throws IOException { - NativeImage img = NativeImage.read(is); - DynamicTexture tex = new DynamicTexture(() -> "lovisual_cosmetic", img); - mc.getTextureManager().register(rawId, tex); - textureId = rawId; - textureLoaded = true; - } - - private void indexBone(CosmeticBone bone) { - boneMap.put(bone.getName().toLowerCase(), bone); - boneMapByUuid.put(bone.getUuid(), bone); - for (CosmeticBone child : bone.getChildren()) indexBone(child); - } - - public CosmeticBone getBone(String name) { - return boneMap.get(name.toLowerCase()); - } - - public CosmeticBone getBoneByUuid(String uuid) { - return boneMapByUuid.get(uuid); - } - - public boolean hasBbAnimations() { - return !bbAnimations.isEmpty(); - } - - public Collection getBbAnimations() { - return bbAnimations.values(); - } - - public CosmeticBbAnimation findBbAnimation(String... candidates) { - for (String candidate : candidates) { - if (candidate == null) continue; - CosmeticBbAnimation anim = bbAnimations.get(candidate.toLowerCase()); - if (anim != null) return anim; - } - for (String candidate : candidates) { - if (candidate == null) continue; - String lower = candidate.toLowerCase(); - for (Map.Entry entry : bbAnimations.entrySet()) { - if (entry.getKey().contains(lower) || lower.contains(entry.getKey())) { - return entry.getValue(); - } - } - } - return null; - } - - public CosmeticBbAnimation getFirstBbAnimation() { - for (String preferred : List.of( - "idling", "idle", "idle_arms", "idlemonsbob", "defaultstate", - "walking", "yap", "painting", "pose", "pose2" - )) { - CosmeticBbAnimation anim = findBbAnimation(preferred); - if (anim != null) return anim; - } - if (bbAnimations.isEmpty()) return null; - return bbAnimations.values().iterator().next(); - } - - public List getRenderRoots() { - return renderRoots.isEmpty() ? rootBones : renderRoots; - } - - public boolean hasCustomLegs() { - return customLegsPresent; - } - - public boolean isBowReplacementAccessory() { - return bowReplacementAccessory; - } - - public boolean isSwordReplacementAccessory() { - return swordReplacementAccessory; - } - - public boolean isBodyOnlyAccessory() { - return type == CosmeticType.ACCESSORY && !bowReplacementAccessory && !swordReplacementAccessory; - } - - public boolean hasRestPoseAnimation() { - return findBbAnimation( - "idling", "idle", "idle_arms", "idlemonsbob", "defaultstate", - "walking", "walkingback", "flywalk" - ) != null; - } - - public boolean shouldSkipBone(CosmeticBone bone) { - if (visibleBoneUuids != null) { - return !visibleBoneUuids.contains(bone.getUuid()); - } - String lower = bone.getName().toLowerCase(); - if (customLegsPresent && lower.contains("vanilla") && (lower.contains("leg") || lower.contains("foot"))) { - return true; - } - if (lower.contains("rightarm_fp") || lower.contains("leftarm_fp")) { - return true; - } - if (hatOnlyAccessory) { - return lower.equals("body") || lower.equals("leftarm") || lower.equals("rightarm") - || lower.equals("leftleg") || lower.equals("rightleg") || lower.equals("root"); - } - return false; - } - - /** - * Derives a standalone pet cosmetic from a single named bone subtree of a parent model - * (e.g. turning a model's "head" bone into its own pet cosmetic). - */ - public static CosmeticModel derivePet(CosmeticModel parent, String boneName, String petFolder, String petName) { - if (parent == null) return null; - CosmeticBone source = parent.findBoneDeep(boneName); - if (source == null) return null; - - CosmeticModel pet = new CosmeticModel( - "derived:" + petFolder.replace('/', '_'), - petName, - "", - parent.getAccentColor(), - petFolder, - CosmeticType.PET, - null, - null - ); - pet.source = parent.source; - pet.textureWidth = parent.textureWidth; - pet.textureHeight = parent.textureHeight; - pet.textureId = parent.textureId; - pet.textureSlots.addAll(parent.textureSlots); - pet.textureLoaded = parent.textureLoaded; - - CosmeticBone root = source.deepClone(); - pet.rootBones.add(root); - pet.renderRoots.add(root); - pet.indexBone(root); - - Set uuids = new HashSet<>(); - pet.collectUuids(root, uuids); - pet.animationBoneFilter = uuids; - - for (Map.Entry entry : parent.bbAnimations.entrySet()) { - String key = entry.getKey().toLowerCase(); - if (key.contains("mons") || key.contains("mini")) { - pet.bbAnimations.put(entry.getKey(), entry.getValue()); - } - } - if (pet.bbAnimations.isEmpty()) { - pet.bbAnimations.putAll(parent.bbAnimations); - } - pet.postProcess(); - return pet; - } - - private void collectUuids(CosmeticBone bone, Set uuids) { - uuids.add(bone.getUuid()); - for (CosmeticBone child : bone.getChildren()) collectUuids(child, uuids); - } - - private CosmeticBone findBoneDeep(String name) { - String lower = name.toLowerCase(); - for (CosmeticBone root : rootBones) { - CosmeticBone found = findBoneDeep(root, lower); - if (found != null) return found; - } - return null; - } - - private static CosmeticBone findBoneDeep(CosmeticBone bone, String lower) { - if (bone.getName().toLowerCase().equals(lower) || bone.getName().toLowerCase().contains(lower)) { - return bone; - } - for (CosmeticBone child : bone.getChildren()) { - CosmeticBone found = findBoneDeep(child, lower); - if (found != null) return found; - } - return null; - } - - private void postProcess() { - customLegsPresent = boneMap.keySet().stream().anyMatch(n -> - n.contains("leftleg") || n.contains("rightleg") || n.contains("leftpaw") || n.contains("rightpaw")) - && boneMap.keySet().stream().noneMatch(n -> n.equals("leftlegvanilla") || n.equals("rightlegvanilla") - || (n.contains("vanilla") && n.contains("leg"))); - - String folder = folderName.toLowerCase(); - bowReplacementAccessory = type == CosmeticType.ACCESSORY && folder.contains("bow"); - swordReplacementAccessory = false; - hatOnlyAccessory = type == CosmeticType.ACCESSORY - && (folder.contains("witch") || folder.contains("hat")) && !bowReplacementAccessory; - wingsOnlyAccessory = type == CosmeticType.ACCESSORY && folder.contains("wing"); - visibleBoneUuids = null; - if (wingsOnlyAccessory) { - visibleBoneUuids = buildWingsBoneChainUuids(); - } - - renderRoots.clear(); - for (CosmeticBone root : rootBones) { - String n = root.getName().toLowerCase(); - if (type == CosmeticType.MODEL) { - if (n.equals("monst3r") || n.equals("skull") || n.endsWith("_fp")) continue; - } - if (type == CosmeticType.PET) { - if (n.equals("arrow") || n.equals("skull")) continue; - } - renderRoots.add(root); - } - if (renderRoots.isEmpty()) { - renderRoots.addAll(rootBones); - } - - applyAccessorySubtreeFilter(); - } - - private void applyAccessorySubtreeFilter() { - if (hatOnlyAccessory) { - CosmeticBone head = findBoneDeep("head"); - CosmeticBone hatBone = findBoneDeep("bone"); - if (head != null && hatBone != null) { - CosmeticBone mount = new CosmeticBone( - head.getName(), head.getUuid(), - new Vector3f(head.getPivot()), new Vector3f(head.getDefaultRotation())); - mount.getChildren().add(hatBone.deepClone()); - renderRoots.clear(); - renderRoots.add(mount); - } - } - } - - private Set buildWingsBoneChainUuids() { - Set keep = new HashSet<>(); - CosmeticBone wings = findBoneDeep("wings"); - if (wings == null) return keep; - collectBoneTreeUuids(wings, keep); - for (CosmeticBone root : rootBones) { - markBonePathToUuid(root, wings.getUuid(), keep); - } - return keep; - } - - private static void collectBoneTreeUuids(CosmeticBone bone, Set uuids) { - uuids.add(bone.getUuid()); - for (CosmeticBone child : bone.getChildren()) { - collectBoneTreeUuids(child, uuids); - } - } - - private static boolean markBonePathToUuid(CosmeticBone bone, String targetUuid, Set keep) { - keep.add(bone.getUuid()); - if (bone.getUuid().equals(targetUuid)) { - return true; - } - for (CosmeticBone child : bone.getChildren()) { - if (markBonePathToUuid(child, targetUuid, keep)) { - return true; - } - } - keep.remove(bone.getUuid()); - return false; - } - - private static Map buildGroupMap(JsonObject bbmodel) { - Map map = new HashMap<>(); - if (!bbmodel.has("groups")) return map; - for (JsonElement el : bbmodel.getAsJsonArray("groups")) { - if (!el.isJsonObject()) continue; - JsonObject group = el.getAsJsonObject(); - if (group.has("uuid")) { - map.put(group.get("uuid").getAsString(), group); - } - } - return map; - } - - private static Vector3f readVec3(JsonObject obj, String key, Vector3f fallback) { - if (obj == null || !obj.has(key)) return new Vector3f(fallback); - JsonArray arr = obj.getAsJsonArray(key); - return new Vector3f(arr.get(0).getAsFloat(), arr.get(1).getAsFloat(), arr.get(2).getAsFloat()); - } - - private static CosmeticBone parseOutliner(JsonElement element, Map elementMap, - Map groupMap, int texW, int texH) { - if (element.isJsonPrimitive() || !element.isJsonObject()) return null; - JsonObject obj = element.getAsJsonObject(); - - String uuid = obj.has("uuid") ? obj.get("uuid").getAsString() : UUID.randomUUID().toString(); - JsonObject group = groupMap.get(uuid); - - String name = "bone"; - if (obj.has("name")) { - name = obj.get("name").getAsString(); - } else if (group != null && group.has("name")) { - name = group.get("name").getAsString(); - } - - Vector3f pivot = obj.has("origin") - ? readVec3(obj, "origin", new Vector3f()) - : readVec3(group, "origin", new Vector3f()); - - Vector3f rotation = obj.has("rotation") - ? readVec3(obj, "rotation", new Vector3f()) - : readVec3(group, "rotation", new Vector3f()); - - CosmeticBone bone = new CosmeticBone(name, uuid, pivot, rotation); - - if (obj.has("children")) { - for (JsonElement child : obj.getAsJsonArray("children")) { - if (child.isJsonPrimitive()) { - String childUuid = child.getAsString(); - JsonObject cubeObj = elementMap.get(childUuid); - if (cubeObj != null) { - CosmeticCube cube = parseCube(cubeObj, texW, texH); - if (cube != null) bone.getCubes().add(cube); - } - } else { - CosmeticBone childBone = parseOutliner(child, elementMap, groupMap, texW, texH); - if (childBone != null) bone.getChildren().add(childBone); - } - } - } - - return bone; - } - - private static CosmeticCube parseCube(JsonObject obj, int texW, int texH) { - try { - JsonArray fromArr = obj.getAsJsonArray("from"); - JsonArray toArr = obj.getAsJsonArray("to"); - Vector3f from = new Vector3f(fromArr.get(0).getAsFloat(), fromArr.get(1).getAsFloat(), fromArr.get(2).getAsFloat()); - Vector3f to = new Vector3f(toArr.get(0).getAsFloat(), toArr.get(1).getAsFloat(), toArr.get(2).getAsFloat()); - - Vector3f rot = new Vector3f(0, 0, 0); - if (obj.has("rotation")) { - JsonArray r = obj.getAsJsonArray("rotation"); - rot.set(r.get(0).getAsFloat(), r.get(1).getAsFloat(), r.get(2).getAsFloat()); - } - - Vector3f origin = new Vector3f(0, 0, 0); - if (obj.has("origin")) { - JsonArray o = obj.getAsJsonArray("origin"); - origin.set(o.get(0).getAsFloat(), o.get(1).getAsFloat(), o.get(2).getAsFloat()); - } - - float inflate = obj.has("inflate") ? obj.get("inflate").getAsFloat() : 0f; - - CosmeticCube.CubeFace[] faces = new CosmeticCube.CubeFace[6]; - String[] faceNames = {"north", "east", "south", "west", "up", "down"}; - - if (obj.has("faces")) { - JsonObject facesObj = obj.getAsJsonObject("faces"); - for (int i = 0; i < faceNames.length; i++) { - String fn = faceNames[i]; - if (!facesObj.has(fn)) continue; - JsonObject face = facesObj.getAsJsonObject(fn); - if (!face.has("uv") || !face.get("uv").isJsonArray()) continue; - JsonArray uv = face.getAsJsonArray("uv"); - if (uv.size() < 4) continue; - int tex = 0; - if (face.has("texture") && !face.get("texture").isJsonNull()) { - JsonElement texEl = face.get("texture"); - if (texEl.isJsonPrimitive() && texEl.getAsJsonPrimitive().isNumber()) { - tex = texEl.getAsInt(); - } - } - float u1 = uv.get(0).getAsFloat(); - float v1 = uv.get(1).getAsFloat(); - float u2 = uv.get(2).getAsFloat(); - float v2 = uv.get(3).getAsFloat(); - if (u2 < u1) { - float t = u1; - u1 = u2; - u2 = t; - } - if (v2 < v1) { - float t = v1; - v1 = v2; - v2 = t; - } - faces[i] = new CosmeticCube.CubeFace(u1, v1, u2, v2, tex); - } - } - - return new CosmeticCube(from, to, rot, origin, inflate, faces, texW, texH); - } catch (Exception e) { - return null; - } - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/cosmetic/model/CosmeticSlot.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/cosmetic/model/CosmeticSlot.java deleted file mode 100644 index 86287c2..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/cosmetic/model/CosmeticSlot.java +++ /dev/null @@ -1,62 +0,0 @@ -package dev.loki.lovisual.features.cosmetic.model; - -/** - * Where a cosmetic is worn. MODEL/PET are whole-body/companion slots (one active - * item each); the remaining six are independent accessory slots that can all be - * active simultaneously (e.g. a hat + wings + a belt at once). - */ -public enum CosmeticSlot { - MODEL("model", "Модель"), - PET("pet", "Питомец"), - HEAD("head", "Голова"), - SHOULDERS("shoulders", "Плечи"), - BACK("back", "Спина"), - WAIST("waist", "Пояс"), - LEGS("legs", "Ноги"), - HELD("held", "В руке"); - - private final String id; - private final String displayName; - - CosmeticSlot(String id, String displayName) { - this.id = id; - this.displayName = displayName; - } - - public String getId() { - return id; - } - - public String getDisplayName() { - return displayName; - } - - public boolean isAccessory() { - return this != MODEL && this != PET; - } - - public static CosmeticSlot fromJson(String raw) { - if (raw == null || raw.isBlank()) return null; - return switch (raw.trim().toLowerCase()) { - case "model", "модель" -> MODEL; - case "pet", "pets", "питомец", "питомцы" -> PET; - case "head", "hat", "голова", "шляпа" -> HEAD; - case "shoulders", "shoulder", "плечи" -> SHOULDERS; - case "back", "спина" -> BACK; - case "waist", "belt", "пояс" -> WAIST; - case "legs", "leg", "ноги" -> LEGS; - case "held", "hand", "рука" -> HELD; - default -> null; - }; - } - - /** Best-effort slot guess from the model's folder name (legacy accessories without a "slot" key). */ - public static CosmeticSlot guessFromFolderName(String folderName) { - if (folderName == null) return HELD; - String folder = folderName.toLowerCase(); - if (folder.contains("witch") || folder.contains("hat")) return HEAD; - if (folder.contains("wing")) return BACK; - if (folder.contains("wheel")) return WAIST; - return HELD; - } -} 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 fe7f0bf..aecdfe8 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 @@ -20,6 +20,7 @@ import dev.loki.lovisual.features.gui.clickgui.settings.render.SettingRenderCont 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.ClickGuiI18n; +import dev.loki.lovisual.features.gui.clickgui.util.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; @@ -282,10 +283,21 @@ public final class SettingsPanelComponent { renderScrollbar(panelScale, palette); } + renderSettingTooltip(mx, my, panelScale, palette); renderHudEditorHints(menuX, menuY, menuW, menuH, panelScale, fadeProgress); renderSettingsPanelHints(menuX, menuY, menuW, menuH, panelScale, fadeProgress); } + /** Пузырь под курсором с {@code .desc}, когда у наведенной строки есть описание. */ + private void renderSettingTooltip(float mx, float my, float panelScale, SettingsGuiPalette tooltipPalette) { + for (SettingHit hit : hits) { + if (inside(mx, my, hit.x(), hit.y(), hit.w(), hit.h())) { + SettingTooltip.render(hit.setting(), mx, my, hit.y() + hit.h(), panelScale, tooltipPalette); + return; + } + } + } + public boolean mouseClicked(float mx, float my, int button, float scale) { if (!isActive()) return false; if (openAnim <= 0.01f) return false; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/core/Setting.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/core/Setting.java index e1bf495..4310b73 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/core/Setting.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/settings/core/Setting.java @@ -34,6 +34,8 @@ public abstract class Setting { private long lastAnimTime = System.nanoTime(); private String cachedDisplayName; private String cachedTranslationKey; + private String cachedDescription; + private String cachedDescriptionBase; private SettingOwner cachedTranslationOwner; private final List commonI18nKeys = new ArrayList<>(); private boolean i18nNameEnabled = true; @@ -167,6 +169,25 @@ public abstract class Setting { return resolved; } + /** + * Hover description from {@code .desc} (e.g. + * setting.cosmeticdisplay.next_cosmetic.desc); null when no key exists — + * never falls back to the raw key, unlike names. + */ + public String getDescription() { + String local = getTranslationKey(); + if (local == null || local.isBlank()) return null; + if (LANG_READY && cachedDescription != null && local.equals(cachedDescriptionBase)) { + return cachedDescription; + } + String translated = resolveFirstTranslation(java.util.List.of(local + ".desc"), false); + if (LANG_READY && translated != null) { + cachedDescription = translated; + cachedDescriptionBase = local; + } + return translated; + } + public SettingOwner getParent() { return parent; } @@ -471,6 +492,8 @@ public abstract class Setting { private void invalidateNameCaches() { cachedDisplayName = null; cachedTranslationKey = null; + cachedDescription = null; + cachedDescriptionBase = null; cachedTranslationOwner = null; cachedOptionDisplayNames.clear(); } 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/SettingTooltip.java new file mode 100644 index 0000000..e7b808d --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/util/SettingTooltip.java @@ -0,0 +1,100 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.clickgui.util; + +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.SettingsGuiPalette; +import dev.loki.lovisual.features.gui.clickgui.layout.screen.settings.render.LayoutRender2D; +import dev.loki.lovisual.features.gui.clickgui.render.ClickGuiRenderer; +import dev.loki.lovisual.features.gui.clickgui.settings.core.Setting; +import dev.loki.lovisual.render.engine.text.render.TextRenderer; +import net.minecraft.client.Minecraft; + +/** + * Hover tooltip for a settings row: renders {@link Setting#getDescription()} + * (the {@code .desc} i18n entry) as a wrapped bubble below the cursor. + * Silent when the setting has no description key. + */ +public final class SettingTooltip { + private static final float MAX_TEXT_W = 170f; + private static final float TEXT_SIZE = 7f; + private static final float PAD = 4f; + private static final float OFFSET = 12f; + + private SettingTooltip() { + } + + /** @param scale ClickGUI ui scale; @param anchorBottom bottom of the hovered row (tooltip flips up when out of screen). */ + public static void render(Setting setting, float mx, float my, float anchorBottom, float scale, SettingsGuiPalette palette) { + if (setting == null || scale <= 0f) return; + String description = setting.getDescription(); + if (description == null || description.isBlank()) return; + + TextRenderer font = ClickGuiRenderer.getInterRegular(); + float size = TEXT_SIZE * scale; + float maxWidth = MAX_TEXT_W * scale; + java.util.List lines = wrap(font, description, maxWidth, size); + if (lines.isEmpty()) return; + + float lineH = ClickGuiRenderer.textHeight(font, size); + float boxW = maxWidth + PAD * 2f * scale; + for (String line : lines) { + boxW = Math.max(boxW, ClickGuiRenderer.textWidth(font, line, size) + PAD * 2f * scale); + } + float boxH = lines.size() * lineH + PAD * 2f * scale; + + float x = clampX(mx + OFFSET * scale, boxW); + float y = anchorBottom + 3f * scale; + float screenH = screenHeight(); + if (y + boxH > screenH - 4f) { + y = Math.max(4f, my - boxH - OFFSET * scale); + } + + int bg = LayoutRender2D.alpha(palette.menuShadow(), 0.92f); + int stroke = LayoutRender2D.alpha(palette.panelStroke(), 0.9f); + float radius = 3.5f * scale; + LayoutRender2D.roundedQuad(x, y, boxW, boxH, radius, bg, bg, bg, bg); + LayoutRender2D.roundedStrokeQuad(x, y, boxW, boxH, radius, 0.5f * scale, + stroke, stroke, stroke, stroke); + + int textColor = palette.moduleDescriptionText(); + float ty = y + PAD * scale; + for (String line : lines) { + ClickGuiRenderer.drawText(font, line, x + PAD * scale, ty, size, textColor, false); + ty += lineH; + } + } + + private static java.util.List wrap(TextRenderer font, String text, float maxWidth, float size) { + java.util.List lines = new java.util.ArrayList<>(); + StringBuilder line = new StringBuilder(); + for (String word : text.split(" ")) { + if (word.isEmpty()) continue; + String candidate = line.isEmpty() ? word : line + " " + word; + if (!line.isEmpty() && ClickGuiRenderer.textWidth(font, candidate, size) > maxWidth) { + lines.add(line.toString()); + line = new StringBuilder(word); + } else { + line = new StringBuilder(candidate); + } + } + if (!line.isEmpty()) lines.add(line.toString()); + return lines; + } + + private static float screenHeight() { + Minecraft mc = Minecraft.getInstance(); + return mc != null ? mc.getWindow().getGuiScaledHeight() : 240f; + } + + private static float clampX(float x, float boxW) { + Minecraft mc = Minecraft.getInstance(); + if (mc == null) return x; + float screenW = mc.getWindow().getGuiScaledWidth(); + return Math.min(Math.max(4f, x), Math.max(4f, screenW - boxW - 4f)); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/core/ModuleManager.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/core/ModuleManager.java index e075be0..93f0d88 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/core/ModuleManager.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/core/ModuleManager.java @@ -223,8 +223,8 @@ public enum ModuleManager { for (Module m : phaseModules) { if (m.isEnabled()) { - try (ProfilerPhase.Scope _ = ProfilerPhase.scope("2d:module:" + m.name()); - RenderProfiler2D.Section ignored = RenderProfiler2D.section("module:" + m.name())) { + try (ProfilerPhase.Scope _ = ProfilerPhase.scope("2d:module:", m.name()); + RenderProfiler2D.Section ignored = RenderProfiler2D.section("module:", m.name())) { if (ModuleExtensionManager.beforeHudRender(m, renderer, textRenderer, ctx, tickDelta)) { m.onRenderHudEngine(renderer, textRenderer, ctx, tickDelta); ModuleExtensionManager.afterHudRender(m, renderer, textRenderer, ctx, tickDelta); @@ -246,8 +246,8 @@ public enum ModuleManager { for (Module m : phaseModules) { if (m.isEnabled()) { - try (ProfilerPhase.Scope phaseScope = ProfilerPhase.scope("2d:module_fg:" + m.name()); - RenderProfiler2D.Section ignored = RenderProfiler2D.section("module_fg:" + m.name())) { + try (ProfilerPhase.Scope phaseScope = ProfilerPhase.scope("2d:module_fg:", m.name()); + RenderProfiler2D.Section ignored = RenderProfiler2D.section("module_fg:", m.name())) { m.onRenderHudEngineForeground(renderer, textRenderer, ctx, tickDelta); } } @@ -295,8 +295,8 @@ public enum ModuleManager { for (Module m : phaseModules) { if (m.isEnabled()) { - try (ProfilerPhase.Scope phaseScope = ProfilerPhase.scope("3d:module:" + m.name()); - RenderProfiler3D.Section ignored = RenderProfiler3D.section("module:" + m.name())) { + try (ProfilerPhase.Scope phaseScope = ProfilerPhase.scope("3d:module:", m.name()); + RenderProfiler3D.Section ignored = RenderProfiler3D.section("module:", m.name())) { if (ModuleExtensionManager.beforeWorldRender(m, renderer, depthRenderer, tickDelta)) { m.onRenderWorldEngine(renderer, depthRenderer, tickDelta); ModuleExtensionManager.afterWorldRender(m, renderer, depthRenderer, tickDelta); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/Predictions.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/Predictions.java index 93073ed..7ac825c 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/Predictions.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/Predictions.java @@ -15,6 +15,7 @@ import dev.loki.lovisual.features.module.core.ModuleInfo; import dev.loki.lovisual.features.module.modules.combat.prediction.Predictions; import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsAim; import dev.loki.lovisual.features.module.modules.combat.predictions.PredictionsSimulation; +import dev.loki.lovisual.features.module.modules.visuals.optimization.FpsOptimizer; import dev.loki.lovisual.features.module.phase.HudPhase; import dev.loki.lovisual.features.module.phase.WorldPhase; import dev.loki.lovisual.features.module.core.Module; @@ -543,6 +544,7 @@ public class Predictions extends Module { ? LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE : LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE_LIQUID_IGNORE; Renderer3D.DepthMode depthMode = walls ? Renderer3D.DepthMode.MAIN : Renderer3D.DepthMode.PRE_DEPTH; + if (FpsOptimizer.bloomDisabled()) return; MeshBuilder mesh = renderer.batchTextured(pipeline, TextureStorage.BLOOM, depthMode); if (mesh == null) return; @@ -645,6 +647,7 @@ public class Predictions extends Module { ? LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE : LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE_LIQUID_IGNORE; Renderer3D.DepthMode depthMode = walls ? Renderer3D.DepthMode.MAIN : Renderer3D.DepthMode.PRE_DEPTH; + if (FpsOptimizer.bloomDisabled()) return; MeshBuilder mesh = renderer.batchTextured(pipeline, TextureStorage.BLOOM, depthMode); if (mesh == null) return; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/TargetESP.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/TargetESP.java index 735e319..b4203dd 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/TargetESP.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/prediction/TargetESP.java @@ -23,6 +23,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.phase.WorldPhase; +import dev.loki.lovisual.features.module.modules.visuals.optimization.FpsOptimizer; import dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspCrystals; import dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspEffects; import dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspModes; @@ -302,12 +303,14 @@ public class TargetESP extends Module { LoVisualRenderPipelines.WORLD_COLORED_LINES_DEPTH, depthMode ); - MeshBuilder bloomMesh = renderer.batchTextured( - LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE_DEPTH, - CRYSTAL_BLOOM, - depthMode - ); - if (additiveMesh == null || fillMesh == null || lineMesh == null || bloomMesh == null) { + MeshBuilder bloomMesh = FpsOptimizer.bloomDisabled() + ? null + : renderer.batchTextured( + LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE_DEPTH, + CRYSTAL_BLOOM, + depthMode + ); + if (additiveMesh == null || fillMesh == null || lineMesh == null) { return; } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspCrystals.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspCrystals.java index 3f79432..618f841 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspCrystals.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/targetesp/TargetEspCrystals.java @@ -57,7 +57,9 @@ public final class TargetEspCrystals { appendCrystalGeometry(fillMesh, targetPos, this, orbitYawRad, 1.0f, applyOpacity(baseColor, 0.3f), true); appendCrystalGeometry(lineMesh, targetPos, this, orbitYawRad, 1.0f, applyOpacity(baseColor, 0.8f), false); appendCrystalGeometry(additiveMesh, targetPos, this, orbitYawRad, 1.2f, applyOpacity(baseColor, 0.3f), true); - appendBloomSphere(bloomMesh, targetPos, this, orbitYawRad, baseColor); + if (bloomMesh != null) { + appendBloomSphere(bloomMesh, targetPos, this, orbitYawRad, baseColor); + } } } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/cosmetics/CosmeticDisplay.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/cosmetics/CosmeticDisplay.java deleted file mode 100644 index bd05b3f..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/cosmetics/CosmeticDisplay.java +++ /dev/null @@ -1,139 +0,0 @@ -package dev.loki.lovisual.features.module.modules.visuals.cosmetics; - -import dev.loki.lovisual.config.values.mode.EnumValue; -import dev.loki.lovisual.config.values.primitive.NumberValue; -import dev.loki.lovisual.features.cosmetic.manager.CosmeticManager; -import dev.loki.lovisual.features.cosmetic.model.CosmeticModel; -import dev.loki.lovisual.features.cosmetic.manager.CosmeticRenderer; -import dev.loki.lovisual.features.cosmetic.manager.CosmeticRepository; -import dev.loki.lovisual.features.cosmetic.model.CosmeticSlot; -import dev.loki.lovisual.features.module.core.Module; -import dev.loki.lovisual.features.module.core.ModuleCategory; -import dev.loki.lovisual.features.module.core.ModuleInfo; -import dev.loki.lovisual.features.module.phase.WorldPhase; -import dev.loki.lovisual.render.engine.renderer.Renderer3D; -import dev.loki.lovisual.util.screen.ClientScreen; -import net.minecraft.client.Minecraft; - -import java.util.EnumMap; -import java.util.List; -import java.util.Map; -import dev.loki.lovisual.features.module.modules.visuals.cosmetics.CosmeticsScreen; - -/** - * Renders the locally equipped cosmetics (per body-zone slot) attached to the local - * player. Right-click the module card in ClickGUI to open {@link CosmeticsScreen} - * (3D preview + per-slot pickers). Offset/rotation/scale overrides are per accessory - * slot (not per model) — see {@link #offsetsFor(CosmeticSlot)}. - */ -@ModuleInfo( - id = "cosmeticdisplay", - displayName = "Cosmetics", - aliases = {"cosmetics"}, - category = ModuleCategory.VISUALS, - description = "Отображает надетую косметику по слотам (модель/питомец/аксессуары)" -) -public final class CosmeticDisplay extends Module { - - /** Per-slot offset/rotation/scale user overrides, applied on top of the base attachment transform. */ - public record SlotOffsetSettings(NumberValue offsetX, NumberValue offsetY, - NumberValue offsetZ, NumberValue rotation, - NumberValue scale) { - } - - private static final CosmeticSlot[] ACCESSORY_SLOTS = { - CosmeticSlot.HEAD, CosmeticSlot.SHOULDERS, CosmeticSlot.BACK, - CosmeticSlot.WAIST, CosmeticSlot.LEGS, CosmeticSlot.HELD - }; - - private final EnumValue activeSlot = - enumSetting("cosmeticsActiveSlot", "active_slot", CosmeticSlot.HEAD, ACCESSORY_SLOTS); - - private final Map slotSettings = buildSlotSettings(); - - { - action("Next Cosmetic", "NONE"); - action("Prev Cosmetic", "NONE"); - action("Open Cosmetics", "NONE"); - } - - private Map buildSlotSettings() { - Map map = new EnumMap<>(CosmeticSlot.class); - for (CosmeticSlot slot : ACCESSORY_SLOTS) { - String prefix = "cosmetics" + capitalize(slot.getId()); - SlotOffsetSettings settings = new SlotOffsetSettings( - visibleWhen(num(prefix + "OffsetX", "offset_x", 0.0f, -1.0f, 1.0f), () -> activeSlot.get() == slot), - visibleWhen(num(prefix + "OffsetY", "offset_y", 0.0f, -1.0f, 1.0f), () -> activeSlot.get() == slot), - visibleWhen(num(prefix + "OffsetZ", "offset_z", 0.0f, -1.0f, 1.0f), () -> activeSlot.get() == slot), - visibleWhen(num(prefix + "Rotation", "rotation", 0.0f, -180.0f, 180.0f), () -> activeSlot.get() == slot), - visibleWhen(num(prefix + "Scale", "scale", 1.0f, 0.1f, 3.0f), () -> activeSlot.get() == slot) - ); - map.put(slot, settings); - } - return map; - } - - private static String capitalize(String s) { - return s.isEmpty() ? s : Character.toUpperCase(s.charAt(0)) + s.substring(1); - } - - public SlotOffsetSettings offsetsFor(CosmeticSlot slot) { - return slotSettings.get(slot); - } - - public CosmeticSlot getActiveSlot() { - return activeSlot.get(); - } - - public void setActiveSlot(CosmeticSlot slot) { - activeSlot.set(slot); - } - - @Override - public void onEnable() { - CosmeticRepository.get().reload(); - } - - @Override - public void onTick() { - if (!isEnabled()) return; - - if (isActionPressedOnce("Next Cosmetic")) { - cycle(1); - } else if (isActionPressedOnce("Prev Cosmetic")) { - cycle(-1); - } else if (isActionPressedOnce("Open Cosmetics")) { - openScreen(); - } - } - - public void openScreen() { - ClientScreen.show(new CosmeticsScreen()); - } - - private void cycle(int direction) { - List models = CosmeticRepository.get().bySlot(CosmeticSlot.MODEL); - if (models.isEmpty()) return; - - CosmeticModel current = CosmeticManager.get().getActive(CosmeticSlot.MODEL); - int index = current == null ? -1 : models.indexOf(current); - int next = Math.floorMod(index + direction, models.size()); - - CosmeticManager.get().setActive(CosmeticSlot.MODEL, models.get(next)); - } - - @Override - public WorldPhase getWorldPhase() { - return WorldPhase.AFTER_POST_PROCESS; - } - - @Override - public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { - Minecraft mc = Minecraft.getInstance(); - if (!isEnabled() || mc.level == null || mc.player == null) return; - - for (CosmeticSlot slot : CosmeticSlot.values()) { - CosmeticRenderer.renderOnPlayer(renderer, mc.player, slot, tickDelta); - } - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/cosmetics/CosmeticsHotspot.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/cosmetics/CosmeticsHotspot.java deleted file mode 100644 index ae551d5..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/cosmetics/CosmeticsHotspot.java +++ /dev/null @@ -1,81 +0,0 @@ -package dev.loki.lovisual.features.module.modules.visuals.cosmetics; - -import dev.loki.lovisual.features.cosmetic.model.CosmeticSlot; - -/** - * Pure geometry for the clickable body-zone rectangles overlaid on the 3D preview. - * Zones are fixed fractions of the preview region height (not bone-projected: the - * inventory-style entity preview renders through its own camera space, so world-camera - * bone projection doesn't apply here). - */ -public final class CosmeticsHotspot { - private final CosmeticSlot slot; - private final float x; - private final float y; - private final float width; - private final float height; - - public CosmeticsHotspot(CosmeticSlot slot, float x, float y, float width, float height) { - this.slot = slot; - this.x = x; - this.y = y; - this.width = width; - this.height = height; - } - - public CosmeticSlot slot() { - return slot; - } - - public float x() { - return x; - } - - public float y() { - return y; - } - - public float width() { - return width; - } - - public float height() { - return height; - } - - public boolean contains(double mouseX, double mouseY) { - return mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + height; - } - - /** - * Builds the four body-zone hotspots for a preview region. BACK reuses the - * SHOULDERS rectangle (selected via a separate front/back view toggle in the screen). - */ - public static CosmeticsHotspot[] forRegion(float regionX, float regionY, float regionW, float regionH, boolean backView) { - float headH = regionH * 0.20f; - float shouldersH = regionH * 0.15f; - float waistH = regionH * 0.15f; - float legsH = regionH - headH - shouldersH - waistH; - - float y0 = regionY; - float y1 = y0 + headH; - float y2 = y1 + shouldersH; - float y3 = y2 + waistH; - - CosmeticSlot shouldersOrBack = backView ? CosmeticSlot.BACK : CosmeticSlot.SHOULDERS; - - return new CosmeticsHotspot[]{ - new CosmeticsHotspot(CosmeticSlot.HEAD, regionX, y0, regionW, headH), - new CosmeticsHotspot(shouldersOrBack, regionX, y1, regionW, shouldersH), - new CosmeticsHotspot(CosmeticSlot.WAIST, regionX, y2, regionW, waistH), - new CosmeticsHotspot(CosmeticSlot.LEGS, regionX, y3, regionW, legsH) - }; - } - - public static CosmeticSlot hitTest(CosmeticsHotspot[] hotspots, double mouseX, double mouseY) { - for (CosmeticsHotspot hotspot : hotspots) { - if (hotspot.contains(mouseX, mouseY)) return hotspot.slot(); - } - return null; - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/cosmetics/CosmeticsScreen.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/cosmetics/CosmeticsScreen.java deleted file mode 100644 index bbd6559..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/cosmetics/CosmeticsScreen.java +++ /dev/null @@ -1,197 +0,0 @@ -package dev.loki.lovisual.features.module.modules.visuals.cosmetics; - -import dev.loki.lovisual.features.cosmetic.model.CosmeticSlot; -import dev.loki.lovisual.features.module.lifecycle.Modules; -import dev.loki.lovisual.features.theme.Theme; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.client.gui.screens.inventory.InventoryScreen; -import net.minecraft.client.input.MouseButtonEvent; -import net.minecraft.network.chat.Component; -import net.minecraft.world.entity.player.Player; -import dev.loki.lovisual.features.module.modules.visuals.cosmetics.CosmeticDisplay; -import dev.loki.lovisual.features.module.modules.visuals.cosmetics.CosmeticsHotspot; -import dev.loki.lovisual.features.module.modules.visuals.cosmetics.CosmeticsSlotPanel; - -/** - * Cosmetics editor: a live 3D player preview on the left (drag to rotate, scroll to - * zoom) with clickable body-zone hotspots, and a per-slot picker/offset panel on the - * right. Opened via the "Open Cosmetics" action / right-click on the module card. - * - * The preview uses {@link InventoryScreen#extractEntityInInventoryFollowsMouse}, which - * derives the entity's yaw/pitch from a "mouse position" argument — we don't feed it the - * real cursor, we feed it a synthetic position offset by {@link #previewYawPx}/ - * {@link #previewPitchPx} so the pose only changes while the user is dragging the preview. - */ -public final class CosmeticsScreen extends Screen { - private static final float DEFAULT_YAW_PX = -40f; - private static final float DEFAULT_PITCH_PX = 15f; - private static final float MAX_PITCH_PX = 60f; - private static final float PANEL_WIDTH = 160f; - private static final float MARGIN = 12f; - - private final CosmeticsSlotPanel slotPanel; - - private float previewYawPx = DEFAULT_YAW_PX; - private float previewPitchPx = DEFAULT_PITCH_PX; - private float previewScale = 1.0f; - private boolean backView = false; - private CosmeticSlot selectedSlot = CosmeticSlot.HEAD; - - private boolean draggingPreview = false; - private double pressX; - private double pressY; - - public CosmeticsScreen() { - super(Component.literal("Cosmetics")); - CosmeticDisplay module = Modules.get(CosmeticDisplay.class); - this.slotPanel = new CosmeticsSlotPanel(module); - } - - @Override - public void extractRenderState(GuiGraphicsExtractor context, int mouseX, int mouseY, float delta) { - int w = context.guiWidth(); - int h = context.guiHeight(); - - Renderer2D.COLOR.begin(); - try { - Renderer2D.COLOR.roundedRect(0, 0, w, h, 0f, Theme.theme().windowBg()); - - float previewW = w - PANEL_WIDTH - MARGIN * 3f; - float previewH = h - MARGIN * 2f; - renderPreview(context, MARGIN, MARGIN, previewW, previewH, delta); - renderHotspots(MARGIN, MARGIN, previewW, previewH, mouseX, mouseY); - - float panelX = MARGIN * 2f + previewW; - slotPanel.render(panelX, MARGIN, PANEL_WIDTH, previewH, selectedSlot, mouseX, mouseY); - } finally { - Renderer2D.COLOR.render(); - } - } - - @Override - public void extractBackground(GuiGraphicsExtractor context, int mouseX, int mouseY, float delta) { - } - - private void renderPreview(GuiGraphicsExtractor context, float x, float y, float width, float height, float tickDelta) { - Player player = Minecraft.getInstance().player; - if (player == null) return; - - int x1 = (int) x; - int y1 = (int) y; - int x2 = (int) (x + width); - int y2 = (int) (y + height); - int size = (int) (Math.min(width, height) * 0.45f * previewScale); - float centerX = x + width / 2f; - float centerY = y + height / 2f; - - InventoryScreen.extractEntityInInventoryFollowsMouse( - context, x1, y1, x2, y2, size, - centerX + previewYawPx, centerY + previewPitchPx, tickDelta, player - ); - } - - private void renderHotspots(float x, float y, float width, float height, double mouseX, double mouseY) { - CosmeticsHotspot[] zones = CosmeticsHotspot.forRegion(x, y, width, height, backView); - int accent = Theme.theme().accent(); - for (CosmeticsHotspot zone : zones) { - boolean hovered = zone.contains(mouseX, mouseY); - boolean active = zone.slot() == selectedSlot; - if (hovered || active) { - int alpha = active ? 70 : 35; - Renderer2D.COLOR.roundedRect(zone.x(), zone.y(), zone.width(), zone.height(), 3f, - (accent & 0x00FFFFFF) | (alpha << 24)); - } - } - } - - @Override - public boolean mouseClicked(MouseButtonEvent click, boolean doubled) { - pressX = click.x(); - pressY = click.y(); - - if (handlePanelClick(click.x(), click.y())) return true; - - if (click.button() == 0 && isInsidePreview((float) click.x(), (float) click.y())) { - draggingPreview = true; - return true; - } - return true; - } - - @Override - public boolean mouseDragged(MouseButtonEvent click, double dragX, double dragY) { - if (!draggingPreview) return false; - previewYawPx += (float) dragX; - previewPitchPx = Math.clamp(previewPitchPx + (float) dragY, -MAX_PITCH_PX, MAX_PITCH_PX); - return true; - } - - @Override - public boolean mouseReleased(MouseButtonEvent click) { - boolean wasDragging = draggingPreview; - draggingPreview = false; - - double moved = Math.hypot(click.x() - pressX, click.y() - pressY); - if (!wasDragging || moved > 4.0) return true; - if (moved <= 4.0) { - handlePreviewClick((float) click.x(), (float) click.y()); - } - return true; - } - - @Override - public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) { - previewScale = Math.clamp(previewScale + (float) verticalAmount * 0.1f, 0.5f, 2.0f); - return true; - } - - private void handlePreviewClick(float mouseX, float mouseY) { - int w = Minecraft.getInstance().getWindow().getGuiScaledWidth(); - int h = Minecraft.getInstance().getWindow().getGuiScaledHeight(); - float previewW = w - PANEL_WIDTH - MARGIN * 3f; - float previewH = h - MARGIN * 2f; - - CosmeticsHotspot[] zones = CosmeticsHotspot.forRegion(MARGIN, MARGIN, previewW, previewH, backView); - CosmeticSlot hit = CosmeticsHotspot.hitTest(zones, mouseX, mouseY); - if (hit == CosmeticSlot.BACK || hit == CosmeticSlot.SHOULDERS) { - backView = !backView; - } - if (hit != null) { - selectSlot(hit); - } - } - - private boolean handlePanelClick(double mouseX, double mouseY) { - int w = Minecraft.getInstance().getWindow().getGuiScaledWidth(); - int h = Minecraft.getInstance().getWindow().getGuiScaledHeight(); - float previewW = w - PANEL_WIDTH - MARGIN * 3f; - float previewH = h - MARGIN * 2f; - float panelX = MARGIN * 2f + previewW; - - return slotPanel.handleClick(panelX, MARGIN, PANEL_WIDTH, selectedSlot, mouseX, mouseY); - } - - private void selectSlot(CosmeticSlot slot) { - selectedSlot = slot; - CosmeticDisplay module = Modules.get(CosmeticDisplay.class); - if (module != null && slot.isAccessory()) { - module.setActiveSlot(slot); - } - } - - private boolean isInsidePreview(float mouseX, float mouseY) { - int w = Minecraft.getInstance().getWindow().getGuiScaledWidth(); - int h = Minecraft.getInstance().getWindow().getGuiScaledHeight(); - float previewW = w - PANEL_WIDTH - MARGIN * 3f; - float previewH = h - MARGIN * 2f; - return mouseX >= MARGIN && mouseX <= MARGIN + previewW && mouseY >= MARGIN && mouseY <= MARGIN + previewH; - } - - @Override - public boolean isPauseScreen() { - return false; - } -} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/cosmetics/CosmeticsSlotPanel.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/cosmetics/CosmeticsSlotPanel.java deleted file mode 100644 index 76b9031..0000000 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/cosmetics/CosmeticsSlotPanel.java +++ /dev/null @@ -1,154 +0,0 @@ -package dev.loki.lovisual.features.module.modules.visuals.cosmetics; - -import dev.loki.lovisual.features.cosmetic.manager.CosmeticManager; -import dev.loki.lovisual.features.cosmetic.model.CosmeticModel; -import dev.loki.lovisual.features.cosmetic.manager.CosmeticRepository; -import dev.loki.lovisual.features.cosmetic.model.CosmeticSlot; -import dev.loki.lovisual.features.theme.Theme; -import dev.loki.lovisual.render.engine.color.RenderColor; -import dev.loki.lovisual.render.engine.renderer.Renderer2D; -import dev.loki.lovisual.render.engine.text.render.TextRenderer; -import dev.loki.lovisual.render.engine.text.font.Fonts; -import dev.loki.lovisual.render.engine.text.font.info.FontInfo; -import net.minecraft.client.resources.language.I18n; - -import java.util.List; -import dev.loki.lovisual.features.module.modules.visuals.cosmetics.CosmeticDisplay; -import dev.loki.lovisual.features.module.modules.visuals.cosmetics.CosmeticsScreen; - -/** - * Right panel of {@link CosmeticsScreen}: model picker for the currently selected - * slot (live-apply on click, "None" always first) plus, for accessory slots, the - * five per-slot offset/rotation/scale sliders declared on {@link CosmeticDisplay}. - */ -public final class CosmeticsSlotPanel { - private static final float ROW_HEIGHT = 20f; - private static final float SLIDER_HEIGHT = 14f; - - private final CosmeticDisplay module; - - CosmeticsSlotPanel(CosmeticDisplay module) { - this.module = module; - } - - void render(float x, float y, float width, float height, CosmeticSlot slot, double mouseX, double mouseY) { - TextRenderer text = Fonts.renderer("Onest", FontInfo.Type.Regular, TextRenderer.get()); - int accent = Theme.theme().accent(); - int muted = Theme.theme().textMuted(); - - text.begin(1.0, true, false); - text.render(slot.getDisplayName(), x, y, new RenderColor(Theme.theme().textPrimary()), false); - text.end(); - - float rowY = y + 16f; - rowY = renderRow(x, rowY, width, "None", isNoneActive(slot), mouseX, mouseY, text, accent, muted); - - for (CosmeticModel model : CosmeticRepository.get().bySlot(slot)) { - boolean active = CosmeticManager.get().getActive(slot) == model; - rowY = renderRow(x, rowY, width, model.getName(), active, mouseX, mouseY, text, accent, muted); - } - - if (slot.isAccessory()) { - renderOffsetSliders(x, rowY + 10f, width, slot, mouseX, mouseY, text, accent, muted); - } - } - - /** @return the y position immediately below the rendered row. */ - private float renderRow(float x, float y, float width, String label, boolean active, - double mouseX, double mouseY, TextRenderer text, int accent, int muted) { - boolean hovered = mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + ROW_HEIGHT; - int bg = active ? Theme.theme().accentSoft() : hovered ? Theme.theme().surfaceHover() : Theme.theme().surface(); - Renderer2D.COLOR.roundedRect(x, y, width, ROW_HEIGHT - 2f, 3f, bg); - - text.begin(1.0, true, false); - text.render(label, x + 6f, y + 6f, new RenderColor(active ? accent : muted), false); - text.end(); - - return y + ROW_HEIGHT; - } - - private void renderOffsetSliders(float x, float y, float width, CosmeticSlot slot, - double mouseX, double mouseY, TextRenderer text, int accent, int muted) { - CosmeticDisplay.SlotOffsetSettings offsets = module.offsetsFor(slot); - if (offsets == null) return; - - float rowY = y; - rowY = slider(x, rowY, width, I18n.get("setting.cosmetics.offset_x"), offsets.offsetX(), text, accent, muted); - rowY = slider(x, rowY, width, I18n.get("setting.cosmetics.offset_y"), offsets.offsetY(), text, accent, muted); - rowY = slider(x, rowY, width, I18n.get("setting.cosmetics.offset_z"), offsets.offsetZ(), text, accent, muted); - rowY = slider(x, rowY, width, I18n.get("setting.cosmetics.rotation"), offsets.rotation(), text, accent, muted); - slider(x, rowY, width, I18n.get("setting.cosmetics.scale"), offsets.scale(), text, accent, muted); - } - - private float slider(float x, float y, float width, String label, - dev.loki.lovisual.config.values.primitive.NumberValue value, - TextRenderer text, int accent, int muted) { - text.begin(0.85, true, false); - text.render(label + ": " + String.format("%.2f", value.get()), x, y, new RenderColor(muted), false); - text.end(); - - float barY = y + 10f; - float min = value.getMin(); - float max = value.getMax(); - float fraction = max > min ? (value.get() - min) / (max - min) : 0f; - - Renderer2D.COLOR.roundedRect(x, barY, width, SLIDER_HEIGHT - 4f, 2f, Theme.theme().surface()); - Renderer2D.COLOR.roundedRect(x, barY, width * fraction, SLIDER_HEIGHT - 4f, 2f, accent); - - return barY + SLIDER_HEIGHT; - } - - /** - * Drag/click on a slider bar; call from the screen's mouse handlers with the same - * rect origin used in {@link #render}. Returns true if the point hit a slider for this slot. - */ - boolean handleSliderDrag(float x, float listBottomY, float width, CosmeticSlot slot, double mouseX, double mouseY) { - if (!slot.isAccessory()) return false; - CosmeticDisplay.SlotOffsetSettings offsets = module.offsetsFor(slot); - if (offsets == null) return false; - - List> sliders = List.of( - offsets.offsetX(), offsets.offsetY(), offsets.offsetZ(), offsets.rotation(), offsets.scale()); - - float rowY = listBottomY + 10f; - for (dev.loki.lovisual.config.values.primitive.NumberValue value : sliders) { - float barY = rowY + 10f; - if (mouseY >= barY && mouseY <= barY + SLIDER_HEIGHT - 4f && mouseX >= x && mouseX <= x + width) { - float fraction = (float) Math.clamp((mouseX - x) / width, 0.0, 1.0); - value.set(value.getMin() + fraction * (value.getMax() - value.getMin())); - return true; - } - rowY += SLIDER_HEIGHT; - } - return false; - } - - boolean handleClick(float x, float y, float width, CosmeticSlot slot, double mouseX, double mouseY) { - List models = CosmeticRepository.get().bySlot(slot); - float rowY = y + 16f; - - if (rowHit(x, rowY, width, mouseX, mouseY)) { - CosmeticManager.get().setActive(slot, null); - return true; - } - rowY += ROW_HEIGHT; - - for (CosmeticModel model : models) { - if (rowHit(x, rowY, width, mouseX, mouseY)) { - CosmeticManager.get().setActive(slot, model); - return true; - } - rowY += ROW_HEIGHT; - } - - return handleSliderDrag(x, rowY, width, slot, mouseX, mouseY); - } - - private boolean rowHit(float x, float y, float width, double mouseX, double mouseY) { - return mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + ROW_HEIGHT; - } - - private boolean isNoneActive(CosmeticSlot slot) { - return CosmeticManager.get().getActive(slot) == null; - } -} 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/HitEffect.java index ae46c61..463efb9 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/HitEffect.java @@ -33,6 +33,7 @@ import org.joml.Vector3f; import dev.loki.lovisual.features.module.core.Module; import dev.loki.lovisual.features.module.core.ModuleCategory; import dev.loki.lovisual.features.module.core.ModuleInfo; +import dev.loki.lovisual.features.module.modules.visuals.optimization.FpsOptimizer; import dev.loki.lovisual.features.module.phase.WorldPhase; import dev.loki.lovisual.mixininterface.entity.ILocalPlayer; import dev.loki.lovisual.mixininterface.entity.IPlayerAttackCooldown; @@ -500,6 +501,7 @@ public class HitEffect extends Module { Quaternionf camRot = RenderState.cameraRotation; if (hitParticleTexture.get() == ParticleTextureMode.BLOOM) { + if (FpsOptimizer.bloomDisabled()) return; MeshBuilder mesh = renderer.batchTextured(pipeline, TextureStorage.FIRE_FLY, depthMode); if (mesh == null) return; for (Particle3D p : hitParticles) { 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/Trails.java index 25b20e8..8d1a3fa 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/Trails.java @@ -24,6 +24,7 @@ import org.joml.Vector3f; import dev.loki.lovisual.features.module.core.Module; import dev.loki.lovisual.features.module.core.ModuleCategory; import dev.loki.lovisual.features.module.core.ModuleInfo; +import dev.loki.lovisual.features.module.modules.visuals.optimization.FpsOptimizer; import dev.loki.lovisual.features.module.phase.WorldPhase; import dev.loki.lovisual.mixininterface.entity.IEntity; import dev.loki.lovisual.mixins.accessors.projectile.PersistentProjectileEntityAccessor; @@ -562,6 +563,7 @@ public class Trails extends Module { Quaternionf camRot = RenderState.cameraRotation; if (particleTexture.get() == ParticleTextureMode.BLOOM) { + if (FpsOptimizer.bloomDisabled()) return; MeshBuilder mesh = renderer.batchTextured(pipeline, TextureStorage.FIRE_FLY, depthMode); if (mesh == null) return; for (Particle3D p : extraParticles) { 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/WorldParticles.java index e1b62e5..f007a38 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/WorldParticles.java @@ -26,6 +26,7 @@ import dev.loki.lovisual.config.common.CommonSettingSchemas; import dev.loki.lovisual.features.module.core.Module; import dev.loki.lovisual.features.module.core.ModuleCategory; import dev.loki.lovisual.features.module.core.ModuleInfo; +import dev.loki.lovisual.features.module.modules.visuals.optimization.FpsOptimizer; import dev.loki.lovisual.features.module.phase.WorldPhase; import dev.loki.lovisual.render.engine.RenderState; import dev.loki.lovisual.render.engine.texture.TextureStorage; @@ -494,7 +495,9 @@ public class WorldParticles extends Module { MeshBuilder distortionMesh = tintedTexturedBatch(renderer, useDepth, depthMode, TextureStorage.FUNNEL_DISTORTION); MeshBuilder eyeMesh = tintedTexturedBatch(renderer, useDepth, depthMode, TextureStorage.FUNNEL_EYE); - MeshBuilder bloomMesh = tintedTexturedBatch(renderer, useDepth, depthMode, TextureStorage.BLOOM); + MeshBuilder bloomMesh = FpsOptimizer.bloomDisabled() + ? null + : tintedTexturedBatch(renderer, useDepth, depthMode, TextureStorage.BLOOM); if (distortionMesh == null && eyeMesh == null && bloomMesh == null) { return; } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/ChinaHatPalette.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/ChinaHatPalette.java index 132ba85..9af0816 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/ChinaHatPalette.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/ChinaHatPalette.java @@ -13,8 +13,6 @@ import dev.loki.lovisual.render.engine.animation.AnimationUtility; import dev.loki.lovisual.util.time.GameClock; import net.minecraft.util.Mth; import net.minecraft.world.entity.player.Player; -import dev.loki.lovisual.features.module.modules.visuals.hats.ChinaHat; -import dev.loki.lovisual.features.module.modules.visuals.hats.TazikHat; /** * Color resolution helpers for ChinaHat. Shared color-math and per-vertex shading, diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/TazikHat.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/TazikHat.java index 0897fe0..8f69839 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/TazikHat.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/hats/TazikHat.java @@ -10,7 +10,6 @@ package dev.loki.lovisual.features.module.modules.visuals.hats; import dev.loki.lovisual.util.time.GameClock; import net.minecraft.client.Minecraft; -import net.minecraft.core.BlockPos; import net.minecraft.util.Mth; import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.entity.player.Player; @@ -34,11 +33,6 @@ import dev.loki.lovisual.render.engine.renderer.Renderer3D; import dev.loki.lovisual.render.engine.uniform.MeshBuilder; import dev.loki.lovisual.render.helpers.player.HeadAnchor; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; -import java.util.Random; - //todo Description @ModuleInfo( id = "tazikhat", @@ -48,14 +42,12 @@ import java.util.Random; public final class TazikHat extends Module { private static final int SEGMENTS = 56; - private static final long DEATH_LIFE_MS = 3600L; private static final double RAINBOW_SPATIAL_SCALE = 12.0; private static final float BOWL_RIM_RADIUS = 0.42f; private static final float BOWL_TOP_RADIUS = 0.245f; private static final float BOWL_HEIGHT = 0.18f; private static final float BOWL_RIM_WIDTH = 0.052f; - private final Random random = new Random(); private final EnumValue renderTarget = enumMode( "render_target", @@ -158,21 +150,6 @@ public final class TazikHat extends Module { 0.0f, 2.5f ); - private final BooleanValue deathFlight = - bool( - "death_flight", - true - ); - private final NumberValue deathFlightPower = - visibleWhen(num( - "death_flight_power", - 1.0f, - 0.25f, - 3.0f - ), deathFlight::get); - private final Map handledDeaths = new HashMap<>(); - private final Map flying = new HashMap<>(); - private static void quadGradient(MeshBuilder mesh, Vec3 p1, int c1, Vec3 p2, int c2, @@ -298,49 +275,6 @@ public final class TazikHat extends Module { return WorldPhase.BEFORE_TRANSLUCENT; } - @Override - public void onTick() { - Minecraft mc = Minecraft.getInstance(); - if (!isEnabled() || mc.level == null || mc.player == null) { - return; - } - - long now = GameClock.millis(); - - for (Player player : mc.level.players()) { - if (player == null || player.isRemoved()) continue; - - int id = player.getId(); - - if (player.isAlive() && player.getHealth() > 0.0f) { - handledDeaths.remove(id); - continue; - } - - if (!deathFlight.get()) continue; - if (handledDeaths.containsKey(id)) continue; - - handledDeaths.put(id, now); - spawnFlyingTazik(player); - } - - handledDeaths.entrySet().removeIf(entry -> now - entry.getValue() > 8000L); - - Iterator> iterator = flying.entrySet().iterator(); - while (iterator.hasNext()) { - FlyingTazik tazik = iterator.next().getValue(); - if (tazik.update()) { - iterator.remove(); - } - } - } - - @Override - public void onDisable() { - handledDeaths.clear(); - flying.clear(); - } - @Override public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { Minecraft mc = Minecraft.getInstance(); @@ -380,8 +314,6 @@ public final class TazikHat extends Module { renderAttached(buffers, player, tickDelta); } } - - renderFlying(buffers, tickDelta); } private boolean shouldRenderSelf() { @@ -425,33 +357,6 @@ public final class TazikHat extends Module { ); } - private void renderFlying(RenderBuffers buffers, float tickDelta) { - long now = GameClock.millis(); - - for (FlyingTazik tazik : flying.values()) { - float life = (now - tazik.createdAtMs) / (float) DEATH_LIFE_MS; - float fade = 1.0f - AnimationUtility.smoothstep(Math.max(0.0f, life - 0.65f) / 0.35f); - - Vec3 pos = new Vec3( - AnimationUtility.lerp(tazik.prevPos.x, tazik.pos.x, tickDelta), - AnimationUtility.lerp(tazik.prevPos.y, tazik.pos.y, tickDelta), - AnimationUtility.lerp(tazik.prevPos.z, tazik.pos.z, tickDelta) - ); - - buildFlyingTazik( - buffers, - pos, - tazik.scale, - tazik.yaw, - tazik.pitch, - tazik.roll, - tazik.palette, - fade, - tazik.seed - ); - } - } - private Wobble resolveWobble(Player player, float tickDelta) { float hurt = Math.max(0.0f, player.hurtTime - tickDelta); float hit = AnimationUtility.easeOutBack(AnimationUtility.clamp01(hurt / 10.0f), 1.25f); @@ -528,28 +433,6 @@ public final class TazikHat extends Module { drawTazikMesh(buffers, rimOuter, rimInner, topOuter, topInner, topCenter, palette, alphaMul); } - private void buildFlyingTazik(RenderBuffers buffers, - Vec3 center, - float size, - float yawDeg, - float pitchDeg, - float rollDeg, - Palette palette, - float alphaMul, - int seed) { - buildTazik( - buffers, - HeadAnchor.world(center), - size, - yawDeg, - pitchDeg, - rollDeg, - palette, - alphaMul, - seed - ); - } - private void drawTazikMesh(RenderBuffers buffers, Vec3[] rimOuter, Vec3[] rimInner, @@ -589,49 +472,6 @@ public final class TazikHat extends Module { } } - private void spawnFlyingTazik(Player player) { - Minecraft mc = Minecraft.getInstance(); - if (player == null) return; - - boolean helmet = hasHelmet(player); - float size = scale.get() * (helmet ? helmetScale.get() : 1.0f); - - HeadAnchor anchor = HeadAnchor.of( - player, - 1.0f, - headTopOffset.get(), - headForwardOffset.get(), - headRightOffset.get() - ); - - Vec3 base = anchor.origin(); - - Vec3 away = mc.player != null - ? base.subtract(mc.player.getEyePosition()) - : new Vec3(randomRange(-1.0f, 1.0f), 0.0, randomRange(-1.0f, 1.0f)); - - if (away.lengthSqr() < 1.0e-5) { - away = new Vec3(randomRange(-1.0f, 1.0f), 0.0, randomRange(-1.0f, 1.0f)); - } - - away = away.normalize(); - - float power = deathFlightPower.get(); - Vec3 velocity = new Vec3( - away.x * randomRange(0.18f, 0.34f) * power + randomRange(-0.08f, 0.08f), - randomRange(0.34f, 0.58f) * power, - away.z * randomRange(0.18f, 0.34f) * power + randomRange(-0.08f, 0.08f) - ); - - flying.put(player.getId(), new FlyingTazik( - base, - velocity, - size, - resolvePalette(player), - random.nextInt() - )); - } - private Palette resolvePalette(Player player) { if (useRelations.get()) { int relation = CategoryService.getColor(player); @@ -713,10 +553,6 @@ public final class TazikHat extends Module { ); } - private float randomRange(float min, float max) { - return min + random.nextFloat() * (max - min); - } - public enum RenderTarget implements EnumValue.IdProvider { SELF("self"), PLAYERS("players"), @@ -764,96 +600,4 @@ public final class TazikHat extends Module { private record Wobble(float pitchDeg, float rollDeg) { } - - private final class FlyingTazik { - private final float scale; - private final Palette palette; - private final int seed; - private final long createdAtMs; - private Vec3 pos; - private Vec3 prevPos; - private Vec3 velocity; - private float yaw; - private float pitch; - private float roll; - private float yawVelocity; - private float pitchVelocity; - private float rollVelocity; - - private FlyingTazik(Vec3 pos, Vec3 velocity, float scale, Palette palette, int seed) { - this.pos = pos; - this.prevPos = pos; - this.velocity = velocity; - this.scale = scale; - this.palette = palette; - this.seed = seed; - this.createdAtMs = GameClock.millis(); - - float power = deathFlightPower.get(); - - this.yaw = randomRange(0.0f, 360.0f); - this.pitch = randomRange(-20.0f, 20.0f); - this.roll = randomRange(-20.0f, 20.0f); - - this.yawVelocity = randomRange(-18.0f, 18.0f) * power; - this.pitchVelocity = randomRange(-22.0f, 22.0f) * power; - this.rollVelocity = randomRange(-24.0f, 24.0f) * power; - } - - private boolean update() { - Minecraft mc = Minecraft.getInstance(); - long age = GameClock.millis() - createdAtMs; - if (age > DEATH_LIFE_MS) { - return true; - } - - prevPos = pos; - - velocity = velocity.add(0.0, -0.034, 0.0); - velocity = new Vec3( - velocity.x * 0.985, - velocity.y * 0.965, - velocity.z * 0.985 - ); - - pos = pos.add(velocity); - - if (mc.level != null) { - BlockPos ground = BlockPos.containing(pos.x, pos.y - 0.035, pos.z); - if (!mc.level.getBlockState(ground).isAir() && velocity.y < 0.0) { - pos = new Vec3(pos.x, ground.getY() + 1.035, pos.z); - - if (Math.abs(velocity.y) > 0.045) { - velocity = new Vec3( - velocity.x * 0.72, - -velocity.y * 0.34, - velocity.z * 0.72 - ); - - pitchVelocity *= 0.72f; - rollVelocity *= 0.72f; - } else { - velocity = new Vec3( - velocity.x * 0.62, - 0.0, - velocity.z * 0.62 - ); - - pitchVelocity *= 0.58f; - rollVelocity *= 0.58f; - } - } - } - - yaw += yawVelocity; - pitch += pitchVelocity; - roll += rollVelocity; - - yawVelocity *= 0.985f; - pitchVelocity *= 0.975f; - rollVelocity *= 0.975f; - - return false; - } - } } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/KineticLyrics.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/KineticLyrics.java index e9d9930..c4192c0 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/KineticLyrics.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/KineticLyrics.java @@ -39,6 +39,11 @@ import dev.loki.lovisual.features.module.modules.visuals.lyrics.LyricParticle3D; ) public final class KineticLyrics extends Module { + /** Прыжок вперёд больше этого — seek: переиндексировать очередь, не спавнить пропущенные строки залпом. */ + private static final long RESYNC_FORWARD_MS = 1500L; + /** Регрессия больше этого — пауза/откат: строки после индекса уже не догнать, нужен resync. */ + private static final long RESYNC_BACKWARD_MS = 750L; + private final EnumValue animation = enumMode("animation", AnimationMode.LYRIC_FLOW, AnimationMode.values()); private final EnumValue colorMode = @@ -135,9 +140,13 @@ public final class KineticLyrics extends Module { // нескольких секунд через Bluetooth), а LRC-таймкоды привязаны к // слышимому звуку. Дефолтный сдвиг назад компенсирует это (решение // совпадает с lrcsnc: у автора ~0.75с задержки для mpd). - long audioTimeMs = Math.max(0L, snapshot.predictedPositionSeconds() * 1000L + timeOffsetMs.get()); + long audioTimeMs = Math.max(0L, snapshot.predictedPositionMs() + timeOffsetMs.get()); - if (lastAudioTimeMs >= 0 && Math.abs(audioTimeMs - lastAudioTimeMs) > 1500L) { + // Любая ощутимая регрессия (pause через truncate-в-секунды MPRIS, короткий seek) — + // иначе индекс остаётся впереди звука и строки молча скипаются до догонки. + if (lastAudioTimeMs >= 0 && audioTimeMs < lastAudioTimeMs - RESYNC_BACKWARD_MS) { + resyncQueue(audioTimeMs); + } else if (lastAudioTimeMs >= 0 && audioTimeMs - lastAudioTimeMs > RESYNC_FORWARD_MS) { resyncQueue(audioTimeMs); } lastAudioTimeMs = audioTimeMs; @@ -157,17 +166,19 @@ public final class KineticLyrics extends Module { lastAudioTimeMs = -1L; isPlaying = false; - LrcLibClient.fetchLyricsAsync(title, artist, debugMode.get()).thenAccept(lines -> { - if (!this.lastTrackKey.equalsIgnoreCase(key)) return; + LrcLibClient.fetchLyricsAsync(title, artist, debugMode.get()).thenAccept(lines -> + Minecraft.getInstance().execute(() -> { + if (!this.lastTrackKey.equalsIgnoreCase(key)) return; - activeParticles.clear(); - rawLyrics.clear(); - lyricsQueue.clear(); - rawLyrics.addAll(lines); - rebuildLyricsQueue(); - currentLyricIndex = 0; - isPlaying = !lyricsQueue.isEmpty(); - }); + activeParticles.clear(); + rawLyrics.clear(); + lyricsQueue.clear(); + rawLyrics.addAll(lines); + rebuildLyricsQueue(); + currentLyricIndex = 0; + isPlaying = !lyricsQueue.isEmpty(); + }) + ); } private void rebuildLyricsQueue() { @@ -226,8 +237,29 @@ public final class KineticLyrics extends Module { activeParticles.removeIf(particle -> particle.isDead(now)); } + /** + * arcSpread is a user-facing "how wide" preference, but it must never exceed what the + * player's actual camera FOV can show, or particles spawn off-screen. Clamp it to the + * real horizontal FOV (derived from vertical FOV + window aspect), with a safety margin + * so text doesn't pop in right at the frustum edge. + */ + private double resolveSpawnSpreadDeg() { + double configured = arcSpread.get(); + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.gameRenderer == null || mc.gameRenderer.mainCamera() == null || mc.getWindow() == null) { + return configured; + } + + float verticalFovDeg = mc.gameRenderer.mainCamera().getFov(); + double aspect = (double) mc.getWindow().getWidth() / Math.max(1, mc.getWindow().getHeight()); + double halfVerticalRad = Math.toRadians(verticalFovDeg) / 2.0; + double halfHorizontalDeg = Math.toDegrees(Math.atan(Math.tan(halfVerticalRad) * aspect)); + + return Math.min(configured, halfHorizontalDeg * 0.85); + } + private Vec3 findNonOverlappingSpawnPos(Vec3 headPos, float cameraYaw) { - double spread = arcSpread.get(); + double spread = resolveSpawnSpreadDeg(); double minD = minDistance.get(); double maxD = Math.max(minD + 0.5, maxDistance.get()); @@ -376,6 +408,7 @@ public final class KineticLyrics extends Module { TYPEWRITER("typewriter"), POP_SCALE("pop_scale"), KINETIC_SLIDE("kinetic_slide"), + LETTER_FALL("letter_fall"), FADE("fade"); private final String id; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/LyricParticle3D.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/LyricParticle3D.java index 12f2e3f..cfe0205 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/LyricParticle3D.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/lyrics/LyricParticle3D.java @@ -9,6 +9,10 @@ import net.minecraft.world.phys.Vec3; public class LyricParticle3D { private static final double BASE_WORLD_SCALE = 0.0016; + private static final float LETTER_FALL_EXIT_START = 0.62f; + private static final float LETTER_FALL_STAGGER = 0.55f; + private static final float LETTER_FALL_DURATION = 0.55f; + private static final double LETTER_FALL_DISTANCE = 0.16; private final String text; private final Vec3 basePosition; @@ -91,6 +95,15 @@ public class LyricParticle3D { animScale = 1.0f - 0.04f * t; } } + case "LETTER_FALL" -> { + if (progress < 0.15f) { + float t = progress / 0.15f; + alpha = easeOutCubic(t); + offsetY = -0.06 * (1.0f - easeOutCubic(t)); + animScale = 0.93f + 0.07f * easeOutBack(t); + } + // exit is handled per-letter below; keep the whole-line alpha/offset at rest here + } case "FADE" -> { if (progress < 0.15f) { alpha = progress / 0.15f; @@ -116,6 +129,11 @@ public class LyricParticle3D { alpha = Mth.clamp(alpha, 0.0f, 1.0f); if (alpha <= 0.001f || displayText == null || displayText.isBlank()) return; + if ("LETTER_FALL".equals(animMode) && progress >= LETTER_FALL_EXIT_START) { + renderLetterFall(renderer, textRenderer, displayText, colorRgb, alpha, offsetY, animScale, userScale, progress); + return; + } + int alphaInt = (int) (alpha * 255.0f); RenderColor color = new RenderColor((alphaInt << 24) | (colorRgb & 0x00FFFFFF)); @@ -131,6 +149,74 @@ public class LyricParticle3D { WorldTextRenderer.drawBillboard(renderer, textRenderer, displayText, anchor, options); } + /** + * Instead of fading the whole line out at once, each letter peels off and tumbles down on + * its own staggered timer (index-based delay), like it's losing grip and falling one by one. + */ + private void renderLetterFall(Renderer3D renderer, + TextRenderer textRenderer, + String text, + int colorRgb, + float baseAlpha, + double baseOffsetY, + float baseAnimScale, + float userScale, + float progress) { + int length = text.length(); + if (length == 0) return; + + double worldScale = BASE_WORLD_SCALE * userScale * baseAnimScale; + WorldTextRenderer.Options measureOptions = WorldTextRenderer.Options.defaults().withWorldScale(worldScale); + double totalWidth = WorldTextRenderer.measureWidth(textRenderer, text, measureOptions); + double cursorX = -totalWidth * 0.5; + + Vec3 lineAnchor = basePosition.add(0.0, baseOffsetY, 0.0); + int alphaBase = Math.round(Mth.clamp(baseAlpha, 0.0f, 1.0f) * 255.0f); + float exitSpan = Math.max(0.001f, 1.0f - LETTER_FALL_EXIT_START); + + for (int i = 0; i < length; i++) { + String letter = text.substring(i, i + 1); + double letterWidth = WorldTextRenderer.measureWidth(textRenderer, letter, measureOptions); + + float letterStart = LETTER_FALL_EXIT_START + (i / (float) length) * LETTER_FALL_STAGGER * exitSpan; + float letterProgress = Mth.clamp((progress - letterStart) / LETTER_FALL_DURATION, 0.0f, 1.0f); + + if (letterProgress > 0.0f) { + float letterAlpha = 1.0f - easeInCubic(letterProgress); + if (letterAlpha > 0.001f) { + double fallY = -LETTER_FALL_DISTANCE * (letterProgress * letterProgress); + double drift = 0.02 * Math.sin(i * 12.9898) * letterProgress; + + int alphaInt = Math.round(letterAlpha * alphaBase); + RenderColor color = new RenderColor((alphaInt << 24) | (colorRgb & 0x00FFFFFF)); + + WorldTextRenderer.Options options = WorldTextRenderer.Options.defaults() + .withColor(color) + .withCentered(false) + .withShadow(true) + .withWorldScale(worldScale) + .withOffset(cursorX + drift, fallY) + .withDepthMode(Renderer3D.DepthMode.MAIN); + + WorldTextRenderer.drawBillboard(renderer, textRenderer, letter, lineAnchor, options); + } + } else { + RenderColor color = new RenderColor((alphaBase << 24) | (colorRgb & 0x00FFFFFF)); + WorldTextRenderer.Options options = WorldTextRenderer.Options.defaults() + .withColor(color) + .withCentered(false) + .withShadow(true) + .withWorldScale(worldScale) + .withOffset(cursorX, 0.0) + .withDepthMode(Renderer3D.DepthMode.MAIN); + + WorldTextRenderer.drawBillboard(renderer, textRenderer, letter, lineAnchor, options); + } + + cursorX += letterWidth; + } + } + private static float easeOutCubic(float t) { return 1.0f - (float) Math.pow(1.0f - t, 3); } 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 new file mode 100644 index 0000000..e522e54 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/optimization/FpsOptimizer.java @@ -0,0 +1,70 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.visuals.optimization; + +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.meta.EventHandler; +import dev.loki.lovisual.features.module.core.Module; +import dev.loki.lovisual.features.module.core.ModuleCategory; +import dev.loki.lovisual.features.module.core.ModuleInfo; +import dev.loki.lovisual.features.module.lifecycle.Modules; + +@ModuleInfo( + id = "fpsoptimizer", + displayName = "FPS Optimizer", + category = ModuleCategory.VISUALS +) +public final class FpsOptimizer extends Module { + + private final BooleanValue disableParticles = + bool("disable_particles", false); + private final BooleanValue disableMenuBackground = + bool("disable_menu_background", false); + private final BooleanValue disableGlass = + bool("disable_glass", false); + private final BooleanValue disableBloom = + bool("disable_bloom", false); + private final BooleanValue entityCulling = + bool("entity_culling", false); + private final NumberValue entityCullDistance = + visibleWhen(num("entity_cull_distance", 64, 8, 256), entityCulling::get); + + public boolean isMenuBackgroundDisabled() { + return isEnabled() && disableMenuBackground.get(); + } + + public boolean isGlassDisabled() { + return isEnabled() && disableGlass.get(); + } + + public boolean isBloomDisabled() { + return isEnabled() && disableBloom.get(); + } + + public boolean isEntityCullingEnabled() { + return isEnabled() && entityCulling.get(); + } + + public int entityCullDistance() { + return entityCullDistance.get(); + } + + /** Cheap call-site guard for renderers that build bloom/glow geometry every frame. */ + public static boolean bloomDisabled() { + FpsOptimizer module = Modules.get(FpsOptimizer.class); + return module != null && module.isBloomDisabled(); + } + + @EventHandler + private void onParticleSpawn(ParticleSpawnEvent event) { + if (!isEnabled() || !disableParticles.get()) return; + event.cancel(); + } +} 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/ShaderSky.java index 3e320d2..f765b86 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/ShaderSky.java @@ -1,6 +1,7 @@ package dev.loki.lovisual.features.module.modules.visuals.weather; import dev.loki.lovisual.config.values.mode.ModeValue; +import dev.loki.lovisual.config.values.primitive.BooleanValue; import dev.loki.lovisual.config.values.primitive.NumberValue; import dev.loki.lovisual.config.values.color.RGBAColorValue; import dev.loki.lovisual.features.module.core.Module; @@ -8,6 +9,7 @@ import dev.loki.lovisual.features.module.core.ModuleCategory; import dev.loki.lovisual.features.module.core.ModuleInfo; import dev.loki.lovisual.features.module.phase.WorldPhase; import dev.loki.lovisual.render.engine.postprocess.render.SkyShaderRenderer; +import dev.loki.lovisual.render.engine.postprocess.render.SkyShaderStarryRenderer; import dev.loki.lovisual.render.engine.renderer.Renderer3D; import net.minecraft.client.Minecraft; @@ -27,12 +29,18 @@ public class ShaderSky extends Module { private static final String SETTING_NEBULA_COLOR1 = "nebula_color1"; private static final String SETTING_NEBULA_COLOR2 = "nebula_color2"; private static final String SETTING_NEBULA_STAR_COLOR = "nebula_star_color"; + private static final String SETTING_STARRY_INTENSITY = "starry_intensity"; + private static final String SETTING_STARRY_DENSITY = "starry_density"; + private static final String SETTING_STARRY_MILKYWAY = "starry_milkyway"; + private static final String SETTING_STARRY_MOON_SIZE = "starry_moon_size"; + private static final String SETTING_STARRY_MOON_COLOR = "starry_moon_color"; + private static final String SETTING_STARRY_STAR_COLOR = "starry_star_color"; private final ModeValue mode = modeSetting( "shaderSkyMode", SETTING_MODE, "Thunder", - "Thunder", "Pulsar", "Sky", "Water", "Caustic", "Nebula" + "Thunder", "Pulsar", "Sky", "Water", "Caustic", "Nebula", "Starry" ); private final NumberValue nebulaIntensity = visibleWhen(num("shaderSkyNebulaIntensity", SETTING_NEBULA_INTENSITY, 1.2f, 0.2f, 3.0f), this::isNebulaMode); @@ -56,6 +64,20 @@ public class ShaderSky extends Module { visibleWhen(num("shaderSkyThunderChance", SETTING_THUNDER_CHANCE, 0.65f, 0.0f, 1.0f), this::isThunderMode); private final NumberValue thunderGlow = visibleWhen(num("shaderSkyThunderGlow", SETTING_THUNDER_GLOW, 1.0f, 0.1f, 3.0f), this::isThunderMode); + private final NumberValue starryIntensity = + visibleWhen(num("shaderSkyStarryIntensity", SETTING_STARRY_INTENSITY, 1.0f, 0.1f, 5.0f), this::isStarryMode); + private final NumberValue starryDensity = + visibleWhen(num("shaderSkyStarryDensity", SETTING_STARRY_DENSITY, 64.0f, 8.0f, 256.0f), this::isStarryMode); + private final NumberValue starryMilkyWay = + visibleWhen(num("shaderSkyStarryMilkyWay", SETTING_STARRY_MILKYWAY, 0.6f, 0.0f, 1.5f), this::isStarryMode); + private final NumberValue starryMoonSize = + visibleWhen(num("shaderSkyStarryMoonSize", SETTING_STARRY_MOON_SIZE, 0.055f, 0.01f, 0.15f), this::isStarryMode); + private final BooleanValue starryMoon = + visibleWhen(bool("shaderSkyStarryMoon", "starry_moon", true), this::isStarryMode); + private final RGBAColorValue starryMoonColor = + visibleWhen(color("shaderSkyStarryMoonColor", SETTING_STARRY_MOON_COLOR, "#FFF5F0DC"), this::isStarryMode); + private final RGBAColorValue starryStarColor = + visibleWhen(color("shaderSkyStarryStarColor", SETTING_STARRY_STAR_COLOR, "#FFFFFFFF"), this::isStarryMode); public String getMode() { return mode.get(); @@ -102,6 +124,10 @@ public class ShaderSky extends Module { return "Nebula".equals(mode.get()); } + private boolean isStarryMode() { + return "Starry".equals(mode.get()); + } + @Override public WorldPhase getWorldPhase() { return WorldPhase.AFTER_POST_PROCESS; @@ -110,6 +136,7 @@ public class ShaderSky extends Module { @Override public void onEnable() { SkyShaderRenderer.reset(); + SkyShaderStarryRenderer.reset(); } @Override @@ -142,6 +169,19 @@ public class ShaderSky extends Module { nebulaColor2.getArgb(), nebulaStarColor.getArgb() ); + } else if (isStarryMode()) { + SkyShaderStarryRenderer.render( + mc, + speed.get(), + starryIntensity.get(), + alpha.get(), + starryDensity.get(), + starryMilkyWay.get(), + starryMoonSize.get(), + starryMoon.get(), + starryMoonColor.getArgb(), + starryStarColor.getArgb() + ); } } } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/wings/WingShapes.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/wings/WingShapes.java new file mode 100644 index 0000000..7f940f6 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/wings/WingShapes.java @@ -0,0 +1,133 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.visuals.wings; + +/** + * Static wing geometry for the Wings module: 8 silhouette shapes (fan of triangles from + * the wing root, each point carries an edge-alpha multiplier) and the pose presets + * (standing / sneaking / elytra-gliding). Ported from the WiNGS reference. + */ +public final class WingShapes { + private WingShapes() { + } + + /** Silhouette vertex: local x (outwards), y (up/down), alphaMul — edge fade of the fan triangle. */ + public record Point(float x, float y, float alphaMul) { + } + + /** Pose-dependent anchoring, spread and flap parameters (all in wing-local units). */ + public record Pose(float preTranslateY, float preTranslateZ, + float anchorY, float anchorZ, + float pitchRotation, float rollRotation, + float openMultiplier, float scaleMultiplier, + float motionSpreadBoost, float flapAmplitude, + float sideOffset, float sideYOffset, float sideZOffset, + float sideRoll, float sidePitch, float flapSpeed) { + } + + public static final Pose POSE_IDLE = + new Pose(0f, 0f, 1.38f, 0.10f, 0f, 0f, 1f, 1f, 0.18f, 4.5f, 0.06f, 0f, 0.02f, -11f, -4f, 0.12f); + public static final Pose POSE_SNEAK = + new Pose(0f, 0f, 0.96f, 0.10f, 18f, 0f, 1f, 1f, 0.18f, 4.5f, 0.06f, 0f, 0.02f, -11f, -4f, 0.12f); + + /** Gliding pose: swept back with progress-scaled pitch toward the flight direction. */ + public static Pose glidePose(float pitch, float flightProgress) { + return new Pose(0.34f, 0.46f, 0f, 0f, flightProgress * (-90f - pitch), 0f, + 0.76f, 0.92f, 0.10f, 0.58f, 0.05f, 0f, 0.06f, -5f, -2f, 0.13f); + } + + public static final String[] TYPES = { + "Angelic", "Dragon", "Butterfly", "Phoenix", "Crystal", "Mechanical", "Fairy", "Demon" + }; + + public static Point[] shape(String type) { + return switch (type) { + case "Dragon" -> DRAGON; + case "Butterfly" -> BUTTERFLY; + case "Phoenix" -> PHOENIX; + case "Crystal" -> CRYSTAL; + case "Mechanical" -> MECHANICAL; + case "Fairy" -> FAIRY; + case "Demon" -> DEMON; + default -> ANGELIC; + }; + } + + public static int[] ribs(Point[] shape) { + int[] template = shape.length <= 10 ? new int[]{2, 4, 6, 8} : new int[]{2, 4, 7, 9, 11}; + int count = 0; + for (int idx : template) { + if (idx < shape.length) count++; + } + int[] out = new int[count]; + int i = 0; + for (int idx : template) { + if (idx < shape.length) out[i++] = idx; + } + return out; + } + + private static final Point[] ANGELIC = { + new Point(0.08f, 0.10f, 0.88f), new Point(0.28f, 0.34f, 0.78f), new Point(0.56f, 0.82f, 0.62f), + new Point(0.86f, 0.30f, 0.52f), new Point(1.14f, 0.46f, 0.40f), new Point(1.24f, 0.04f, 0.30f), + new Point(1.02f, -0.18f, 0.28f), new Point(1.18f, -0.64f, 0.22f), new Point(0.86f, -0.46f, 0.20f), + new Point(0.80f, -0.98f, 0.14f), new Point(0.54f, -0.74f, 0.16f), new Point(0.30f, -1.16f, 0.12f), + new Point(0.10f, -0.54f, 0.18f) + }; + + private static final Point[] DRAGON = { + new Point(0.10f, 0.12f, 0.90f), new Point(0.22f, 0.40f, 0.80f), new Point(0.48f, 0.72f, 0.65f), + new Point(0.80f, 0.60f, 0.55f), new Point(1.10f, 0.70f, 0.42f), new Point(1.30f, 0.30f, 0.35f), + new Point(1.20f, -0.10f, 0.30f), new Point(1.05f, -0.50f, 0.25f), new Point(0.70f, -0.35f, 0.22f), + new Point(0.50f, -0.70f, 0.18f), new Point(0.20f, -0.50f, 0.15f), new Point(0.05f, -0.30f, 0.20f) + }; + + private static final Point[] BUTTERFLY = { + new Point(0.12f, 0.15f, 0.92f), new Point(0.30f, 0.50f, 0.85f), new Point(0.50f, 0.90f, 0.70f), + new Point(0.70f, 0.80f, 0.60f), new Point(0.85f, 0.55f, 0.50f), new Point(0.75f, 0.20f, 0.45f), + new Point(0.55f, -0.15f, 0.40f), new Point(0.40f, -0.60f, 0.30f), new Point(0.25f, -0.85f, 0.20f), + new Point(0.12f, -0.65f, 0.25f), new Point(0.06f, -0.35f, 0.30f) + }; + + private static final Point[] PHOENIX = { + new Point(0.10f, 0.14f, 0.90f), new Point(0.25f, 0.45f, 0.82f), new Point(0.52f, 0.78f, 0.68f), + new Point(0.82f, 0.50f, 0.55f), new Point(1.15f, 0.55f, 0.42f), new Point(1.28f, 0.15f, 0.32f), + new Point(1.20f, -0.25f, 0.28f), new Point(1.10f, -0.55f, 0.24f), new Point(1.25f, -0.85f, 0.18f), + new Point(0.90f, -0.65f, 0.16f), new Point(0.60f, -0.90f, 0.14f), new Point(0.30f, -0.70f, 0.12f), + new Point(0.08f, -0.40f, 0.16f) + }; + + private static final Point[] CRYSTAL = { + new Point(0.15f, 0.10f, 0.85f), new Point(0.40f, 0.35f, 0.75f), new Point(0.70f, 0.60f, 0.60f), + new Point(1.00f, 0.40f, 0.50f), new Point(0.85f, 0.10f, 0.45f), new Point(1.10f, -0.15f, 0.35f), + new Point(0.90f, -0.45f, 0.30f), new Point(0.65f, -0.30f, 0.25f), new Point(0.45f, -0.60f, 0.20f), + new Point(0.20f, -0.40f, 0.22f), new Point(0.08f, -0.15f, 0.30f) + }; + + private static final Point[] MECHANICAL = { + new Point(0.08f, 0.08f, 0.90f), new Point(0.20f, 0.25f, 0.82f), new Point(0.45f, 0.40f, 0.70f), + new Point(0.70f, 0.35f, 0.58f), new Point(0.95f, 0.25f, 0.48f), new Point(0.90f, 0.00f, 0.40f), + new Point(1.10f, -0.15f, 0.32f), new Point(0.80f, -0.30f, 0.28f), new Point(0.55f, -0.20f, 0.25f), + new Point(0.40f, -0.45f, 0.20f), new Point(0.15f, -0.30f, 0.22f), new Point(0.05f, -0.10f, 0.28f) + }; + + private static final Point[] FAIRY = { + new Point(0.10f, 0.12f, 0.90f), new Point(0.25f, 0.38f, 0.82f), new Point(0.42f, 0.65f, 0.68f), + new Point(0.55f, 0.70f, 0.58f), new Point(0.60f, 0.45f, 0.50f), new Point(0.50f, 0.15f, 0.42f), + new Point(0.38f, -0.10f, 0.35f), new Point(0.30f, -0.35f, 0.28f), new Point(0.18f, -0.45f, 0.22f), + new Point(0.08f, -0.25f, 0.26f) + }; + + private static final Point[] DEMON = { + new Point(0.10f, 0.12f, 0.88f), new Point(0.25f, 0.38f, 0.80f), new Point(0.55f, 0.65f, 0.65f), + new Point(0.85f, 0.50f, 0.52f), new Point(1.15f, 0.55f, 0.40f), new Point(1.25f, 0.20f, 0.32f), + new Point(1.10f, -0.10f, 0.28f), new Point(1.30f, -0.45f, 0.22f), new Point(1.15f, -0.70f, 0.18f), + new Point(0.85f, -0.55f, 0.16f), new Point(0.55f, -0.85f, 0.14f), new Point(0.25f, -0.65f, 0.12f), + new Point(0.08f, -0.35f, 0.18f) + }; +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/wings/Wings.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/wings/Wings.java new file mode 100644 index 0000000..be71182 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/wings/Wings.java @@ -0,0 +1,133 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.visuals.wings; + +import dev.loki.lovisual.config.values.color.RGBAColorValue; +import dev.loki.lovisual.config.values.mode.ModeValue; +import dev.loki.lovisual.config.values.primitive.BooleanValue; +import dev.loki.lovisual.config.values.primitive.NumberValue; +import dev.loki.lovisual.features.module.core.Module; +import dev.loki.lovisual.features.module.core.ModuleCategory; +import dev.loki.lovisual.features.module.core.ModuleInfo; +import dev.loki.lovisual.features.module.phase.WorldPhase; +import dev.loki.lovisual.features.relations.PlayerRelations; +import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import net.minecraft.client.Minecraft; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Items; + +/** + * Cosmetic wings on player backs (ported from the WiNGS reference): 8 silhouette types, + * pose-driven flapping (idle/sneak/glide), friends/all-players visibility. Geometry data + * lives in {@link WingShapes}, mesh building in {@link WingsRenderer}. + */ +@ModuleInfo( + id = "wings", + displayName = "Wings", + category = ModuleCategory.VISUALS, + description = "Крылья на спине игрока: 8 типов формы, анимация взмахов" +) +public final class Wings extends Module { + + private final ModeValue wingType = modeSetting( + "wingsType", "type", "Angelic", WingShapes.TYPES); + private final ModeValue target = modeSetting( + "wingsTarget", "target", "Self", "Self", "Friends", "All Players"); + private final NumberValue scale = + num("wingsScale", "scale", 1.0f, 0.3f, 3.0f); + private final BooleanValue flapping = + bool("wingsFlapping", "flapping", true); + private final NumberValue flapStrength = + visibleWhen(num("wingsFlapStrength", "flap_strength", 30.0f, 5.0f, 60.0f), flapping::get); + private final NumberValue flapSpeed = + visibleWhen(num("wingsFlapSpeed", "flap_speed", 3.0f, 0.5f, 8.0f), flapping::get); + private final BooleanValue throughWalls = + bool("wingsThroughWalls", "through_walls", false); + private final BooleanValue glow = bool("wingsGlow", "glow", true); + private final BooleanValue outline = bool("wingsOutline", "outline", true); + private final BooleanValue ribs = bool("wingsRibs", "ribs", true); + private final RGBAColorValue color = + color("wingsColor", "color", "#FFFFFFDC"); + + private float selfYaw; + private boolean selfYawReady; + + @Override + public WorldPhase getWorldPhase() { + return WorldPhase.END_MAIN; + } + + @Override + public void onDisable() { + selfYawReady = false; + } + + @Override + public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); + if (!isEnabled() || mc.level == null || mc.player == null || renderer == null) return; + + WingsRenderer.Style style = new WingsRenderer.Style( + wingType.get(), scale.get(), flapping.get(), flapStrength.get(), flapSpeed.get(), + color.getArgb(), glow.get(), outline.get(), ribs.get(), !throughWalls.get()); + + if (showsSelf() && !mc.options.getCameraType().isFirstPerson() + && mc.player.isAlive() && !hasElytra(mc.player)) { + renderOne(renderer, mc.player, tickDelta, style, true); + } + if (showsOthers()) { + for (Player player : mc.level.players()) { + if (player == mc.player || !player.isAlive() || hasElytra(player)) continue; + if (!showsOthers(player)) continue; + renderOne(renderer, player, tickDelta, style, false); + } + } + } + + private void renderOne(Renderer3D renderer, Player player, float tickDelta, + WingsRenderer.Style style, boolean isSelf) { + float targetYaw = Mth.rotLerp(tickDelta, player.yBodyRotO, player.yBodyRot); + float bodyYaw; + if (isSelf) { + if (!selfYawReady || player.tickCount < 2) { + selfYaw = targetYaw; + selfYawReady = true; + } else { + selfYaw += Mth.clamp(Mth.wrapDegrees(targetYaw - selfYaw), -14f, 14f); + } + bodyYaw = selfYaw; + } else { + bodyYaw = targetYaw; + } + WingsRenderer.render(renderer, player, tickDelta, bodyYaw, style); + } + + private boolean showsSelf() { + String t = target.get(); + return "Self".equals(t) || "All Players".equals(t); + } + + private boolean showsOthers() { + return "Friends".equals(target.get()) || "All Players".equals(target.get()); + } + + private boolean showsOthers(Player player) { + String t = target.get(); + if ("All Players".equals(t)) return true; + if ("Friends".equals(t)) { + return PlayerRelations.get().isFriend(player.getGameProfile().name()); + } + return false; + } + + private static boolean hasElytra(Player player) { + return player.getItemBySlot(EquipmentSlot.CHEST).is(Items.ELYTRA); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/wings/WingsRenderer.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/wings/WingsRenderer.java new file mode 100644 index 0000000..a37bd3b --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/wings/WingsRenderer.java @@ -0,0 +1,197 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.visuals.wings; + +import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; +import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.phys.Vec3; +import org.joml.Matrix4f; + +/** + * Draws a pair of wings on a player's back: anchor transform (position/body yaw/pose, + * mirroring the reference PoseStack chain via joml matrices) then the fan of glow/core/ + * fill triangles + outline + ribs per side. Ported from the WiNGS reference onto + * Renderer3D batches (world-space vertices, pipelines supply depth behavior). + */ +public final class WingsRenderer { + private static final float DEFAULT_SPREAD = 8.0f; + private static final int DEFAULT_ALPHA = 220; + + private WingsRenderer() { + } + + /** Per-frame style knobs resolved by the module. */ + public record Style(String type, float scale, boolean flapping, float flapStrength, float flapSpeed, + int baseColorArgb, boolean glow, boolean outline, boolean ribs, boolean depthTest) { + } + + public static void render(Renderer3D renderer, Player player, float tickDelta, float bodyYaw, Style s) { + if (renderer == null || player == null) return; + + WingShapes.Pose pose = resolvePose(player, tickDelta); + if (pose == null) return; + + WingShapes.Point[] shape = WingShapes.shape(s.type()); + int[] ribIndices = WingShapes.ribs(shape); + Vec3 pos = player.getPosition(tickDelta); + float move = Mth.clamp((float) player.getDeltaMovement().horizontalDistance() * 10f, 0f, 1f); + + float flap = 0f; + if (s.flapping()) { + float effectiveSpeed = pose.flapSpeed() * (s.flapSpeed() / 3.0f); + float effectiveAmp = pose.flapAmplitude() * (s.flapStrength() / 30.0f); + flap = Mth.sin((player.tickCount + tickDelta) * effectiveSpeed) * effectiveAmp; + } + float open = (DEFAULT_SPREAD + flap + move * pose.motionSpreadBoost()) * pose.openMultiplier(); + float ws = s.scale() * pose.scaleMultiplier(); + + int glowColor = interpolate(s.baseColorArgb(), 0xFFFFFF, 0.28f); + int coreColor = interpolate(s.baseColorArgb(), 0xFFFFFF, 0.55f); + + Meshes meshes = new Meshes(renderer, s.depthTest()); + for (float side = -1f; side <= 1f; side += 2f) { + drawWingSide(meshes, pos, bodyYaw, pose, shape, ribIndices, side, open, ws, s, glowColor, coreColor); + } + } + + private static final class Meshes { + final MeshBuilder glow; + final MeshBuilder fill; + final MeshBuilder outline; + final Renderer3D.DepthMode depthMode; + + Meshes(Renderer3D renderer, boolean depthTest) { + this.depthMode = depthTest ? Renderer3D.DepthMode.MAIN : Renderer3D.DepthMode.NONE; + this.glow = renderer.batch(depthTest + ? LoVisualRenderPipelines.WORLD_COLORED_ADDITIVE_DEPTH + : LoVisualRenderPipelines.WORLD_COLORED_ADDITIVE, depthMode); + this.fill = renderer.batch(depthTest + ? LoVisualRenderPipelines.WORLD_COLORED_DEPTH + : LoVisualRenderPipelines.WORLD_COLORED, depthMode); + this.outline = renderer.batch(depthTest + ? LoVisualRenderPipelines.WORLD_COLORED_LINES_DEPTH + : LoVisualRenderPipelines.WORLD_COLORED_LINES, depthMode); + } + } + + /** + * Builds the matrix chain of the reference: translate(player) · Y(180-yaw) · preTranslate · + * X(pitch) · Z(roll) · translate(anchor) · scale · [per side] translate(side) · Y(side·open) · + * Z(side·sideRoll) · X(sidePitch). Wing-local verts are (side·x, y, 0). + */ + private static void drawWingSide(Meshes m, Vec3 pos, float bodyYaw, WingShapes.Pose pose, + WingShapes.Point[] shape, int[] ribIndices, float side, + float open, float ws, Style s, int glowColor, int coreColor) { + Matrix4f mat = new Matrix4f() + .translate((float) pos.x, (float) pos.y, (float) pos.z) + .rotateY((float) Math.toRadians(180f - bodyYaw)) + .translate(0f, pose.preTranslateY(), pose.preTranslateZ()) + .rotateX((float) Math.toRadians(pose.pitchRotation())) + .rotateZ((float) Math.toRadians(pose.rollRotation())) + .translate(0f, pose.anchorY(), pose.anchorZ()) + .scale(ws) + .translate(side * pose.sideOffset(), pose.sideYOffset(), pose.sideZOffset()) + .rotateY((float) Math.toRadians(side * open)) + .rotateZ((float) Math.toRadians(side * pose.sideRoll())) + .rotateX((float) Math.toRadians(pose.sidePitch())); + + int baseAlpha = DEFAULT_ALPHA; + int rootGlow = setAlpha(glowColor, (int) (baseAlpha * 0.22f)); + int rootCore = setAlpha(coreColor, (int) (baseAlpha * 0.26f)); + int rootFill = setAlpha(s.baseColorArgb(), baseAlpha); + int edgeFill = setAlpha(s.baseColorArgb(), 10); + int rootRib = setAlpha(glowColor, Math.max(8, (int) (alphaOf(glowColor) * 0.75f))); + + fanLayer(m.glow, mat, shape, 1.22f, side, rootGlow, setAlpha(glowColor, 0)); + fanLayer(m.glow, mat, shape, 0.84f, side, rootCore, setAlpha(coreColor, 0)); + fanLayer(m.fill, mat, shape, 1.0f, side, rootFill, edgeFill); + if (s.outline()) outline(m.outline, mat, shape, side, 1.0f, setAlpha(s.baseColorArgb(), (int) (baseAlpha * 0.62f))); + if (s.ribs()) ribs(m.glow, mat, shape, ribIndices, side, rootRib); + } + + private static void fanLayer(MeshBuilder mesh, Matrix4f mat, WingShapes.Point[] shape, + float layerScale, float side, int rootColor, int edgeColor) { + if (mesh == null) return; + int rootIdx = put(mesh, mat, 0f, 0f, 0f, rootColor); + for (int i = 0; i < shape.length; i++) { + WingShapes.Point cur = shape[i]; + WingShapes.Point next = shape[(i + 1) % shape.length]; + int curAlpha = setAlpha(edgeColor, Math.round(alphaOf(edgeColor) * cur.alphaMul())); + int nextAlpha = setAlpha(edgeColor, Math.round(alphaOf(edgeColor) * next.alphaMul())); + int i1 = put(mesh, mat, side * cur.x() * layerScale, cur.y() * layerScale, 0f, curAlpha); + int i2 = put(mesh, mat, side * next.x() * layerScale, next.y() * layerScale, 0f, nextAlpha); + mesh.triangle(rootIdx, i1, i2); + } + } + + private static void outline(MeshBuilder mesh, Matrix4f mat, WingShapes.Point[] shape, + float side, float scale, int color) { + if (mesh == null) return; + int first = put(mesh, mat, side * shape[0].x() * scale, shape[0].y() * scale, 0f, color); + int prev = first; + for (int i = 1; i < shape.length; i++) { + WingShapes.Point p = shape[i]; + int idx = put(mesh, mat, side * p.x() * scale, p.y() * scale, 0f, color); + mesh.line(prev, idx); + prev = idx; + } + mesh.line(prev, first); + } + + private static void ribs(MeshBuilder mesh, Matrix4f mat, WingShapes.Point[] shape, int[] ribIndices, + float side, int color) { + if (mesh == null) return; + int rootIdx = put(mesh, mat, 0f, 0f, 0f, color); + for (int idx : ribIndices) { + if (idx >= shape.length) continue; + WingShapes.Point p = shape[idx]; + int tip = put(mesh, mat, side * p.x() * 0.96f, p.y() * 0.96f, 0f, + setAlpha(color, Math.max(0, Math.round(alphaOf(color) * p.alphaMul())))); + mesh.line(rootIdx, tip); + } + } + + private static int put(MeshBuilder mesh, Matrix4f mat, float x, float y, float z, int argb) { + float wx = mat.m00() * x + mat.m01() * y + mat.m02() * z + mat.m03(); + float wy = mat.m10() * x + mat.m11() * y + mat.m12() * z + mat.m13(); + float wz = mat.m20() * x + mat.m21() * y + mat.m22() * z + mat.m23(); + mesh.vec3(wx, wy, wz).color((argb >>> 16) & 0xFF, (argb >>> 8) & 0xFF, argb & 0xFF, (argb >>> 24) & 0xFF); + return mesh.next(); + } + + private static WingShapes.Pose resolvePose(Player player, float tickDelta) { + if (player.isFallFlying()) { + float flightTicks = (float) player.getFallFlyingTicks() + tickDelta; + float progress = Mth.clamp(flightTicks * flightTicks / 100f, 0f, 1f); + float pitch = Mth.lerp(tickDelta, player.xRotO, player.getXRot()); + return WingShapes.glidePose(pitch, progress); + } + if (player.isInWater()) return null; + if (player.isShiftKeyDown()) return WingShapes.POSE_SNEAK; + return WingShapes.POSE_IDLE; + } + + static int setAlpha(int color, int a) { + return (Mth.clamp(a, 0, 255) << 24) | (color & 0x00FFFFFF); + } + + static int alphaOf(int color) { + return (color >>> 24) & 0xFF; + } + + /** Linear lerp of rgb toward a white-ish target rgb (alpha preserved from source). */ + static int interpolate(int fromArgb, int targetRgb, float t) { + int r = Math.round(((fromArgb >>> 16) & 0xFF) + ((((targetRgb >>> 16) & 0xFF)) - ((fromArgb >>> 16) & 0xFF)) * t); + int g = Math.round(((fromArgb >>> 8) & 0xFF) + ((((targetRgb >>> 8) & 0xFF)) - ((fromArgb >>> 8) & 0xFF)) * t); + int b = Math.round((fromArgb & 0xFF) + ((targetRgb & 0xFF) - (fromArgb & 0xFF)) * t); + return setAlpha((r << 16) | (g << 8) | b, alphaOf(fromArgb)); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/blaze3d/encoder/CommandEncoderMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/blaze3d/encoder/CommandEncoderMixin.java index ecdf7a4..ccf6323 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/blaze3d/encoder/CommandEncoderMixin.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/blaze3d/encoder/CommandEncoderMixin.java @@ -62,15 +62,30 @@ public abstract class CommandEncoderMixin { return depthAttachment == null ? null : depthAttachment.textureView(); } + @Unique + private static Boolean lovisual$vulkanBackendCached; + @Unique private static boolean lovisual$isVulkanBackend() { - try { - if (RenderSystem.getDevice() == null || RenderSystem.getDevice().getDeviceInfo() == null) return false; - String backendName = RenderSystem.getDevice().getDeviceInfo().backendName(); - return backendName != null && backendName.toLowerCase(java.util.Locale.ROOT).contains("vulkan"); - } catch (Throwable ignored) { - return false; + // submitRenderPass runs every frame; device info is static per session — resolve once. + Boolean cached = lovisual$vulkanBackendCached; + if (cached != null) { + return cached; } + boolean vulkan = false; + try { + if (RenderSystem.getDevice() != null && RenderSystem.getDevice().getDeviceInfo() != null) { + String backendName = RenderSystem.getDevice().getDeviceInfo().backendName(); + vulkan = backendName != null && backendName.toLowerCase(java.util.Locale.ROOT).contains("vulkan"); + } + } catch (Throwable ignored) { + vulkan = false; + } + // Only cache once the device exists; before init, retry next call. + if (RenderSystem.getDevice() != null) { + lovisual$vulkanBackendCached = vulkan; + } + return vulkan; } @Inject(method = "createRenderPass(Lcom/mojang/blaze3d/systems/RenderPassDescriptor;)Lcom/mojang/blaze3d/systems/RenderPass;", at = @At("RETURN")) diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/iris/render/IrisRenderingPipelineFinalizeMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/iris/render/IrisRenderingPipelineFinalizeMixin.java index 1858b38..478166a 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/iris/render/IrisRenderingPipelineFinalizeMixin.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/iris/render/IrisRenderingPipelineFinalizeMixin.java @@ -34,7 +34,11 @@ public abstract class IrisRenderingPipelineFinalizeMixin { @Inject(method = "beginTranslucents", at = @At("HEAD"), remap = false) private void lovisual$capturePreTranslucentDepth(CallbackInfo ci) { - PreTranslucentDepth.capture(); + if (PreTranslucentDepth.takeRequested()) { + PreTranslucentDepth.capture(); + } else { + PreTranslucentDepth.reset(); + } } @Inject(method = "finalizeLevelRendering", at = @At("HEAD"), remap = false) diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/entity/EntityRenderDispatcherMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/entity/EntityRenderDispatcherMixin.java new file mode 100644 index 0000000..4e09e6c --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/render/entity/EntityRenderDispatcherMixin.java @@ -0,0 +1,42 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.mixins.render.entity; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.culling.Frustum; +import net.minecraft.client.renderer.entity.EntityRenderDispatcher; +import net.minecraft.world.entity.Entity; +import org.spongepowered.asm.mixin.Mixin; +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.features.module.lifecycle.Modules; +import dev.loki.lovisual.features.module.modules.visuals.optimization.FpsOptimizer; + +@Mixin(EntityRenderDispatcher.class) +public abstract class EntityRenderDispatcherMixin { + + @Inject(method = "shouldRender(Lnet/minecraft/world/entity/Entity;Lnet/minecraft/client/renderer/culling/Frustum;DDD)Z", at = @At("HEAD"), cancellable = true) + private void lovisual$cullEntity(E entity, Frustum frustum, double camX, double camY, double camZ, CallbackInfoReturnable cir) { + FpsOptimizer fpsOptimizer = Modules.get(FpsOptimizer.class); + if (fpsOptimizer == null || !fpsOptimizer.isEntityCullingEnabled()) return; + + Minecraft mc = Minecraft.getInstance(); + if (entity == mc.player) return; + + double maxDistance = fpsOptimizer.entityCullDistance(); + if (entity.distanceToSqr(camX, camY, camZ) > maxDistance * maxDistance) { + cir.setReturnValue(false); + return; + } + + if (!frustum.isVisible(entity.getBoundingBox())) { + cir.setReturnValue(false); + } + } +} 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 700a5df..9e33dfc 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 @@ -275,6 +275,10 @@ public abstract class LevelRendererMixin { if (IrisRuntime.isShaderpackRendererActive()) { return; } + if (!PreTranslucentDepth.takeRequested()) { + PreTranslucentDepth.reset(); + return; + } RenderTarget mainFramebuffer = lovisual$getFramebuffer(lovisual$asRenderTargetHandle(mainFramebufferHandle)); if (mainFramebuffer == null) { Minecraft mc = Minecraft.getInstance(); 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/ParticleManagerMixin.java index cfc0e00..84b1509 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/ParticleManagerMixin.java @@ -7,21 +7,30 @@ package dev.loki.lovisual.mixins.render.level; +import net.minecraft.client.particle.Particle; import net.minecraft.client.particle.ParticleEngine; import net.minecraft.core.particles.ParticleOptions; import org.spongepowered.asm.mixin.Mixin; 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.util.player.effect.heuristics.StatusEffectHeuristics; @Mixin(ParticleEngine.class) public class ParticleManagerMixin { - @Inject(method = "createParticle(Lnet/minecraft/core/particles/ParticleOptions;DDDDDD)Lnet/minecraft/client/particle/Particle;", at = @At("HEAD")) - private void lovisual$heuristicParticle(ParticleOptions parameters, double x, double y, double z, double velocityX, double velocityY, double velocityZ, CallbackInfoReturnable cir) { + @Inject(method = "createParticle(Lnet/minecraft/core/particles/ParticleOptions;DDDDDD)Lnet/minecraft/client/particle/Particle;", at = @At("HEAD"), cancellable = true) + private void lovisual$heuristicParticle(ParticleOptions parameters, double x, double y, double z, double velocityX, double velocityY, double velocityZ, CallbackInfoReturnable cir) { // dev.loki.lovisual.util.logging.DebugLog.info("ParticleManager add type=%s pos=%.2f %.2f %.2f", parameters.getType().toString(), x, y, z); StatusEffectHeuristics.handle(parameters, x, y, z); + + ParticleSpawnEvent event = new ParticleSpawnEvent(parameters, x, y, z); + Events.BUS.post(event); + if (event.isCancelled()) { + cir.setReturnValue(null); + } } } 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 cf7d69c..602c5bc 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 @@ -98,6 +98,20 @@ public abstract class GameRendererMixin implements IrisFinalizedSceneRenderer { private final PoseStack lovisual$matrices = new PoseStack(); @Unique private final PoseStack lovisual$postMatrices = new PoseStack(); + // Per-frame scratch (GameRenderer is a singleton; consumers copy by value): + // avoids ~7 Matrix4f/PoseStack/Quaternionf allocations per frame in the hot render path. + @Unique + private final PoseStack lovisual$effectMatrices = new PoseStack(); + @Unique + private final Matrix4f lovisual$screenProjectionScratch = new Matrix4f(); + @Unique + private final Matrix4f lovisual$modelViewScratch = new Matrix4f(); + @Unique + private final Matrix4f lovisual$inverseBobScratch = new Matrix4f(); + @Unique + private final Matrix4f lovisual$cameraPositionScratch = new Matrix4f(); + @Unique + private final org.joml.Quaternionf lovisual$conjugateScratch = new org.joml.Quaternionf(); @Shadow @Final private Minecraft minecraft; @@ -232,10 +246,10 @@ public abstract class GameRendererMixin implements IrisFinalizedSceneRenderer { CameraRenderState cameraRenderState = gameRenderState.levelRenderState.cameraRenderState; if (cameraRenderState == null || cameraRenderState.viewRotationMatrix == null || cameraRenderState.projectionMatrix == null) return; - PoseStack effectMatrices = new PoseStack(); - Matrix4f renderProjection = lovisual$buildWorldProjection(fov, effectMatrices, tickDelta); + lovisual$effectMatrices.setIdentity(); + Matrix4f renderProjection = lovisual$buildWorldProjection(fov, lovisual$effectMatrices, tickDelta); AspectRatio.applyToProjection(renderProjection); - Matrix4f screenProjection = new Matrix4f(cameraRenderState.projectionMatrix); + Matrix4f screenProjection = lovisual$screenProjectionScratch.set(cameraRenderState.projectionMatrix); LoVisualWorldMatrices.capture( cameraRenderState.viewRotationMatrix, renderProjection, @@ -263,7 +277,8 @@ public abstract class GameRendererMixin implements IrisFinalizedSceneRenderer { boolean useCapturedMatrices = capturedPosition != null && capturedProjection != null; Matrix4f position = useCapturedMatrices ? capturedPosition - : new Matrix4f().rotation(mainCamera.rotation().conjugate(new org.joml.Quaternionf())); + : lovisual$cameraPositionScratch.identity().rotation( + mainCamera.rotation().conjugate(lovisual$conjugateScratch)); RenderState.rendering3D = true; RenderState.tickDelta = tickDelta; Vec3 capturedCameraPos = LoVisualWorldMatrices.cameraPosition(); @@ -343,7 +358,7 @@ public abstract class GameRendererMixin implements IrisFinalizedSceneRenderer { tickCounter.getGameTimeDeltaTicks(), tickCounter.getRealtimeDeltaTicks(), projection, - new Matrix4f(mv) + lovisual$modelViewScratch.set(mv) ); try (RenderPhaseScope lovisual$beforeTranslucentPhase = LoVisualRenderSystem.phase(RenderPhase.WORLD_BEFORE_TRANSLUCENT)) { RenderState.worldProjection.set(projection); @@ -351,8 +366,7 @@ public abstract class GameRendererMixin implements IrisFinalizedSceneRenderer { RenderState.frustum.prepare(RenderState.cameraPos.x, RenderState.cameraPos.y, RenderState.cameraPos.z); if (!useCapturedMatrices) { - Matrix4f inverseBob = new Matrix4f(lovisual$matrices.last().pose()).invert(); - mv.mul(inverseBob); + mv.mul(lovisual$inverseBobScratch.set(lovisual$matrices.last().pose()).invert()); } lovisual$renderer.begin(); @@ -572,7 +586,8 @@ public abstract class GameRendererMixin implements IrisFinalizedSceneRenderer { boolean useCapturedMatrices = capturedPosition != null && capturedProjection != null; Matrix4f position = useCapturedMatrices ? capturedPosition - : new Matrix4f().rotation(mainCamera.rotation().conjugate(new org.joml.Quaternionf())); + : lovisual$cameraPositionScratch.identity().rotation( + mainCamera.rotation().conjugate(lovisual$conjugateScratch)); RenderState.rendering3D = true; RenderState.tickDelta = tickDelta; @@ -612,7 +627,7 @@ public abstract class GameRendererMixin implements IrisFinalizedSceneRenderer { tickCounter.getGameTimeDeltaTicks(), tickCounter.getRealtimeDeltaTicks(), projection, - new Matrix4f(mv) + lovisual$modelViewScratch.set(mv) ); try (RenderPhaseScope lovisual$postWorldPhase = LoVisualRenderSystem.phase(RenderPhase.WORLD_POST_PRE_HAND)) { RenderState.worldProjection.set(projection); @@ -620,8 +635,7 @@ public abstract class GameRendererMixin implements IrisFinalizedSceneRenderer { RenderState.frustum.prepare(RenderState.cameraPos.x, RenderState.cameraPos.y, RenderState.cameraPos.z); if (!useCapturedMatrices) { - Matrix4f inverseBob = new Matrix4f(lovisual$postMatrices.last().pose()).invert(); - mv.mul(inverseBob); + mv.mul(lovisual$inverseBobScratch.set(lovisual$postMatrices.last().pose()).invert()); } lovisual$postRenderer.begin(); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/depth/PreTranslucentDepth.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/depth/PreTranslucentDepth.java index c3113f7..49ca718 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/depth/PreTranslucentDepth.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/depth/PreTranslucentDepth.java @@ -35,6 +35,26 @@ public enum PreTranslucentDepth { private static int msaaDepthCopyH = -1; private static boolean captured; private static boolean capturedMsaa; + private static boolean requestedNextFrame; + + /** + * The full-screen depth copy in {@link #captureFrom} is expensive, and its capture point + * (before the translucent groups in the main pass) runs BEFORE any PRE_DEPTH consumer renders + * in the frame. Consumers ({@link #getDepthView()}, {@link #getDepthViewFor(GpuTextureView)}) + * therefore declare demand here; the next frame's capture consults it via {@link #takeRequested()}. + * One-frame latency is invisible: until the copy exists, getters fall back to the live + * framebuffer/WorldSceneDepth views. + */ + public static void requestForNextFrame() { + requestedNextFrame = true; + } + + /** @return and clears the demand flag set by {@link #requestForNextFrame()}. */ + public static boolean takeRequested() { + boolean requested = requestedNextFrame; + requestedNextFrame = false; + return requested; + } public static void captureFrom(RenderTarget source) { if (source == null) return; @@ -97,6 +117,7 @@ public enum PreTranslucentDepth { } public static GpuTextureView getDepthView() { + requestForNextFrame(); if (MsaaWorldTarget.isActive() && capturedMsaa && msaaDepthView != null) { return msaaDepthView; } @@ -115,6 +136,7 @@ public enum PreTranslucentDepth { } public static GpuTextureView getDepthViewFor(GpuTextureView colorAttachment) { + requestForNextFrame(); int requiredSamples = samples(colorAttachment); if (requiredSamples > 1 && capturedMsaa 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 44e07d7..c645d4e 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 @@ -152,6 +152,8 @@ public enum LoVisualRenderPipelines { // SkyShader nebula public static final Identifier SHADER_SKY_SHADER_NEBULA_FRAG = shader("sky_shader_nebula.frag"); public static final Identifier SHADER_SKY_SHADER_CAUSTIC_FRAG = shader("sky_shader_caustic.frag"); + // SkyShader starry (procedural stars + milky way + round moon) + public static final Identifier SHADER_SKY_SHADER_STARRY_FRAG = shader("sky_shader_starry.frag"); private static final List PIPELINES = new ArrayList<>(); @@ -174,6 +176,8 @@ public enum LoVisualRenderPipelines { public static final RenderPipeline WORLD_TEXTURED_DEPTH = add(WorldPipelines.textured("world_textured_depth", true, BlendFunction.TRANSLUCENT)); /** Depth test (GEQUAL); additive; colored triangles. */ public static final RenderPipeline WORLD_COLORED_ADDITIVE_DEPTH = add(WorldPipelines.colored("world_colored_additive_depth", true, PipelineTemplates.ADDITIVE)); + /** No depth test; additive; colored triangles. */ + public static final RenderPipeline WORLD_COLORED_ADDITIVE = add(WorldPipelines.colored("world_colored_additive", false, PipelineTemplates.ADDITIVE)); /** No depth test; translucent; world decal quads with SDF fill/stroke. */ public static final RenderPipeline WORLD_DECAL_SDF = add(WorldPipelines.decalSdf("world_decal_sdf", false)); /** Depth test (GEQUAL); translucent; world decal quads with SDF fill/stroke. */ @@ -246,6 +250,7 @@ public enum LoVisualRenderPipelines { public static final RenderPipeline SKY_SHADER_WATER = add(ScreenPipelines.screen("sky_shader_water", SHADER_SKY_SHADER_WATER_FRAG, "SkyShader")); public static final RenderPipeline SKY_SHADER_CAUSTIC = add(ScreenPipelines.screen("sky_shader_caustic", SHADER_SKY_SHADER_CAUSTIC_FRAG, "SkyShader")); public static final RenderPipeline SKY_SHADER_NEBULA = add(ScreenPipelines.screen("sky_shader_nebula", SHADER_SKY_SHADER_NEBULA_FRAG, "SkyShaderData")); + public static final RenderPipeline SKY_SHADER_STARRY = add(ScreenPipelines.screen("sky_shader_starry", SHADER_SKY_SHADER_STARRY_FRAG, "SkyShaderStarry")); /** Fullscreen main menu background (pos2). */ public static final RenderPipeline MENU_BACKGROUND_AURORA = add(ScreenPipelines.screen("menu_background_aurora", SHADER_MENU_BACKGROUND_AURORA_FRAG, "MenuBackground")); /** Fullscreen postprocess (pos2). */ diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/PostProcessGraph.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/PostProcessGraph.java index 0b7c13a..07e4b41 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/PostProcessGraph.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/graph/PostProcessGraph.java @@ -60,7 +60,7 @@ public final class PostProcessGraph implements AutoCloseable { LoVisualRhi rhi, GraphCopy copy) { if (!hasActivePass(phase, context)) return false; - try (RenderCostProfiler.Scope ignoredGraph = RenderCostProfiler.postPass("graph:" + phase)) { + try (RenderCostProfiler.Scope ignoredGraph = RenderCostProfiler.postPass("graph:", phase.toString())) { if (!resources.prepare(phase, tickDelta)) return false; GpuTextureView mainColor = resources.mainColor(); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/render/MenuBackgroundRenderer.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/render/MenuBackgroundRenderer.java index 1bbb240..0a7ee07 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/render/MenuBackgroundRenderer.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/render/MenuBackgroundRenderer.java @@ -24,6 +24,8 @@ 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.ui.MenuBackgroundUniforms; +import dev.loki.lovisual.features.module.lifecycle.Modules; +import dev.loki.lovisual.features.module.modules.visuals.optimization.FpsOptimizer; public enum MenuBackgroundRenderer { ; @@ -41,6 +43,8 @@ public enum MenuBackgroundRenderer { public static void render(Minecraft mc, boolean aurora) { if (mc == null) return; + FpsOptimizer fpsOptimizer = Modules.get(FpsOptimizer.class); + if (fpsOptimizer != null && fpsOptimizer.isMenuBackgroundDisabled()) return; if (Renderer2D.isDeferredExtractRecording()) { deferredPending = true; deferredAurora = aurora; 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 new file mode 100644 index 0000000..7c5973c --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/postprocess/render/SkyShaderStarryRenderer.java @@ -0,0 +1,138 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.render.engine.postprocess.render; + +import com.mojang.blaze3d.ProjectionType; +import com.mojang.blaze3d.buffers.GpuBufferSlice; +import com.mojang.blaze3d.pipeline.RenderTarget; +import com.mojang.blaze3d.systems.RenderSystem; +import dev.loki.lovisual.render.engine.RenderState; +import dev.loki.lovisual.features.module.modules.visuals.weather.WorldTweaks; +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 net.minecraft.client.Camera; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.ProjectionMatrixBuffer; +import net.minecraft.util.Util; +import org.joml.Matrix4f; +import org.joml.Quaternionf; + +/** + * Fullscreen "Starry" sky pass: procedural star field + milky way band + a round moon + * placed opposite the sun (driven by the world day cycle). Mirrors the scaffolding of + * {@link SkyShaderRenderer#renderNebula}; split out to keep both files within the size limit. + */ +public enum SkyShaderStarryRenderer { + ; + private static final Matrix4f IDENTITY = new Matrix4f(); + private static ProjectionMatrixBuffer projection; + private static long startMillis = -1L; + + public static void reset() { + startMillis = -1L; + } + + public static void render(Minecraft mc, float speed, float intensity, float alpha, + float starDensity, float milkyWayIntensity, float moonSize, + boolean showMoon, int moonColorArgb, int starColorArgb) { + if (mc == null || mc.level == null || mc.player == null) return; + + RenderTarget fb = mc.gameRenderer.mainRenderTarget(); + if (fb == null) return; + + int w = mc.getWindow().getWidth(); + int h = mc.getWindow().getHeight(); + if (w <= 0 || h <= 0) return; + + FullScreenRenderer.ensureInit(); + if (startMillis < 0) startMillis = Util.getMillis(); + float time = (Util.getMillis() - startMillis) / 1000.0f; + + Camera cam = mc.gameRenderer.mainCamera(); + Quaternionf rot = cam.rotation(); + float fov = (float) mc.options.fov().get().intValue(); + + float[] moonDir = moonDirection(mc); + if (!showMoon) { + // The shader clamps the angular radius, so hiding the moon means pointing it + // below the horizon where horizonFade discards it. + moonDir = new float[]{0f, -1f, 0f}; + } + float moonPhase = ((WorldTweaks.getServerTimeOfDay() / 24000f) % 8f) / 8f; + + SkyShaderStarryUniforms.update(w, h, time, speed, intensity, alpha, fov, + rot.x(), rot.y(), rot.z(), rot.w(), + moonDir[0], moonDir[1], moonDir[2], moonSize, + moonPhase, milkyWayIntensity, starDensity, + channel(moonColorArgb, 16), channel(moonColorArgb, 8), channel(moonColorArgb, 0), + channel(starColorArgb, 16), channel(starColorArgb, 8), channel(starColorArgb, 0)); + + var mv = RenderSystem.getModelViewStack(); + boolean pushedModelView = false; + GpuBufferSlice prevProj = RenderSystem.getProjectionMatrixBuffer(); + ProjectionType prevProjType = RenderSystem.getProjectionType(); + Matrix4f prevMeshProjection = MeshRenderer.projection(); + boolean prevRendering3D = RenderState.rendering3D; + + try { + mv.pushMatrix(); + pushedModelView = true; + mv.identity(); + + if (projection == null) { + projection = new ProjectionMatrixBuffer("lovisual-sky-shader-starry-projection"); + } + Matrix4f proj = IDENTITY.identity(); + RenderSystem.setProjectionMatrix(projection.getBuffer(proj), ProjectionType.PERSPECTIVE); + MeshRenderer.setProjection(proj); + RenderState.rendering3D = false; + + FullScreenRenderer.begin("LoVisual Fullscreen Pass") + .attachment(fb) + .pipeline(LoVisualRenderPipelines.SKY_SHADER_STARRY) + .uniform("SkyShaderStarry", SkyShaderStarryUniforms.get()) + .end(); + } finally { + if (pushedModelView) { + mv.popMatrix(); + } + MeshRenderer.setProjection(prevMeshProjection); + RenderState.rendering3D = prevRendering3D; + if (prevProj != null && prevProjType != null) { + RenderSystem.setProjectionMatrix(prevProj, prevProjType); + } + } + } + + /** + * World-space direction to the moon: opposite the sun along its day arc + * (CustomSkyboxRenderer sunDir = (-sin a, cos a, 0), so moon = (sin a, -cos a, 0)). + * Sun angle comes from the last rendered sky pass; fallback derives it from the day clock. + * + * @return normalized xyz + */ + private static float[] moonDirection(Minecraft mc) { + float sunAngleRad; + if (CustomSkyboxRenderer.hasLastSunAngle()) { + sunAngleRad = CustomSkyboxRenderer.getLastSunAngle(); + } else { + float timeOfDay = (WorldTweaks.getServerTimeOfDay() % 24000L) / 24000f; + sunAngleRad = timeOfDay * (float) (Math.PI * 2.0); + } + float x = (float) Math.sin(sunAngleRad); + float y = (float) -Math.cos(sunAngleRad); + return new float[]{x, y, 0f}; + } + + private static float channel(int argb, int shift) { + return ((argb >>> shift) & 0xFF) / 255f; + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/config/ProfilerPhase.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/config/ProfilerPhase.java index ca12032..5fadcd7 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/config/ProfilerPhase.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/config/ProfilerPhase.java @@ -17,6 +17,11 @@ public enum ProfilerPhase { return new Scope(DevProfilerBridge.closeable("ProfilerPhase", "scope", new Class[]{String.class}, label)); } + /** Two-part label: concatenation happens only when a dev profiler is attached. */ + public static Scope scope(String prefix, String suffix) { + return DEV ? scope(prefix + suffix) : Scope.NOOP; + } + public static void begin(String label) { if (!DEV) return; DevProfilerBridge.invoke("ProfilerPhase", "begin", new Class[]{String.class}, label); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/frame/RenderCostProfiler.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/frame/RenderCostProfiler.java index 14aedf5..964a14f 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/frame/RenderCostProfiler.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/frame/RenderCostProfiler.java @@ -33,6 +33,15 @@ public enum RenderCostProfiler { return DevProfilerBridge.bool("RenderCostProfiler", "isEnabled", false, new Class[0]); } + /** + * Single static-field read for call sites that build concatenated labels + * ("graph:" + phase, "3d:module:" + name) every frame — the string work is + * free when no dev profiler jar is attached. + */ + public static boolean profiling() { + return DEV; + } + public static boolean isConfigured() { if (!DEV) return false; return DevProfilerBridge.bool("RenderCostProfiler", "isConfigured", false, new Class[0]); @@ -69,6 +78,11 @@ public enum RenderCostProfiler { return new Scope(DevProfilerBridge.closeable("RenderCostProfiler", "postPass", new Class[]{String.class}, label)); } + /** Two-part label: concatenation happens only when a dev profiler is attached. */ + public static Scope postPass(String prefix, String suffix) { + return DEV ? postPass(prefix + suffix) : Scope.NOOP; + } + public static Scope rhiDraw(String label) { if (!DEV) return Scope.NOOP; return new Scope(DevProfilerBridge.closeable("RenderCostProfiler", "rhiDraw", new Class[]{String.class}, label)); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/render/RenderProfiler2D.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/render/RenderProfiler2D.java index c2f0bfb..70c5cdd 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/render/RenderProfiler2D.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/render/RenderProfiler2D.java @@ -39,6 +39,11 @@ public enum RenderProfiler2D { return new Section(DevProfilerBridge.closeable("RenderProfiler2D", "section", new Class[]{String.class}, name)); } + /** Two-part label: concatenation happens only when a dev profiler is attached. */ + public static Section section(String prefix, String suffix) { + return DEV ? section(prefix + suffix) : Section.NOOP; + } + public static List getDebugLines() { if (!DEV) return List.of(); return DevProfilerBridge.lines("RenderProfiler2D", "getDebugLines", new Class[0]); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/render/RenderProfiler3D.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/render/RenderProfiler3D.java index 2615d0f..1bcc422 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/render/RenderProfiler3D.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/profiler/render/RenderProfiler3D.java @@ -39,6 +39,11 @@ public enum RenderProfiler3D { return new Section(DevProfilerBridge.closeable("RenderProfiler3D", "section", new Class[]{String.class}, name)); } + /** Two-part label: concatenation happens only when a dev profiler is attached. */ + public static Section section(String prefix, String suffix) { + return DEV ? section(prefix + suffix) : Section.NOOP; + } + public static List getDebugLines() { if (!DEV) return List.of(); return DevProfilerBridge.lines("RenderProfiler3D", "getDebugLines", new Class[0]); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/fx/Blur2D.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/fx/Blur2D.java index 23804de..b3d8a53 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/fx/Blur2D.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/renderer2d/fx/Blur2D.java @@ -10,6 +10,8 @@ package dev.loki.lovisual.render.engine.renderer.renderer2d.fx; import com.mojang.blaze3d.pipeline.RenderTarget; import com.mojang.blaze3d.textures.GpuSampler; import com.mojang.blaze3d.textures.GpuTextureView; +import dev.loki.lovisual.features.module.lifecycle.Modules; +import dev.loki.lovisual.features.module.modules.visuals.optimization.FpsOptimizer; import dev.loki.lovisual.render.engine.postprocess.post.PostProcessManager; import dev.loki.lovisual.render.engine.renderer.Renderer2D; import dev.loki.lovisual.render.engine.renderer.renderer2d.Batch2D; @@ -46,6 +48,8 @@ public final class Blur2D { /** Main framebuffer color texture plus the shared post-process sampler. */ public static BlurSource source() { + FpsOptimizer fpsOptimizer = Modules.get(FpsOptimizer.class); + if (fpsOptimizer != null && fpsOptimizer.isGlassDisabled()) return null; Minecraft mc = Minecraft.getInstance(); if (mc == null) return null; RenderTarget fb = mc.gameRenderer.mainRenderTarget(); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/UiBlurResources.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/UiBlurResources.java index a1d141b..b35f979 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/UiBlurResources.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/renderer/ui/blur/UiBlurResources.java @@ -115,13 +115,22 @@ public final class UiBlurResources { UiDeferredScheduler.deferAction(UiBlurResources::prepareCapturedWorldBlur); } + /** + * Full-screen main-color copy — only when liquid glass was actually requested this frame + * (set by getOrCreateBlur/liquid-glass text recording, reset each extract frame). Skipping + * it when no HUD element uses glass removes a per-frame fullscreen copy from the "dead tax". + */ public static void captureWorldSource() { + if (!liquidGlassBlurRequested) { + worldSourceReady = false; + return; + } Minecraft minecraft = Minecraft.getInstance(); if (minecraft == null) return; RenderTarget framebuffer = minecraft.gameRenderer.mainRenderTarget(); TextureTarget source = ensureGlassSource(minecraft); worldSourceReady = copyMainColor(framebuffer, source); - if (!worldSourceReady || !liquidGlassBlurRequested || source == null) return; + if (!worldSourceReady || source == null) return; prepareCapturedWorldBlur(); } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/GlStencilShapeClipBackend.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/GlStencilShapeClipBackend.java index 9f74180..a285f2b 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/GlStencilShapeClipBackend.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/rhi/backend/gl/clip/GlStencilShapeClipBackend.java @@ -96,13 +96,16 @@ public final class GlStencilShapeClipBackend implements ShapeClipBackend { if (offscreenPassBypassDepth > 0) { resetNativeStateOnly(); - checkGlErrors("beginRenderPass(offscreen-bypass)"); + if (DebugLog.isStencilDebugEnabled()) { + checkGlErrors("beginRenderPass(offscreen-bypass)"); + } return; } boolean shouldPrepare = renderPassAttachmentRequired || mode != Mode.DISABLED || clearRequested; if (!shouldPrepare) { - checkGlErrors("beginRenderPass(no-clip)"); + // Hot path (every vanilla/Sodium pass with clip idle): glGetError syncs with the + // driver — skip it unless explicitly debugging stencil. return; } @@ -127,6 +130,9 @@ public final class GlStencilShapeClipBackend implements ShapeClipBackend { @Override public void endRenderPass() { + // Capture before endRenderPass resets renderPassAttachmentRequired below. + boolean errorsOfInterest = mode != Mode.DISABLED || renderPassAttachmentRequired + || DebugLog.isStencilDebugEnabled(); resetNativeStateOnly(); currentColorView = null; currentDepthView = null; @@ -138,7 +144,9 @@ public final class GlStencilShapeClipBackend implements ShapeClipBackend { renderPassAttachmentRequired = false; attachmentReason = "none"; } - checkGlErrors(offscreenPassBypassDepth > 0 ? "endRenderPass(offscreen-bypass)" : "endRenderPass"); + if (errorsOfInterest) { + checkGlErrors(offscreenPassBypassDepth > 0 ? "endRenderPass(offscreen-bypass)" : "endRenderPass"); + } } @Override @@ -250,12 +258,16 @@ public final class GlStencilShapeClipBackend implements ShapeClipBackend { public void applyNativeState() { if (offscreenPassBypassDepth > 0) { resetNativeStateOnly(); - checkGlErrors("applyNativeState(offscreen-bypass)"); + if (DebugLog.isStencilDebugEnabled()) { + checkGlErrors("applyNativeState(offscreen-bypass)"); + } return; } if (mode == Mode.DISABLED) { resetNativeStateOnly(); - checkGlErrors("applyNativeState(disabled)"); + if (DebugLog.isStencilDebugEnabled()) { + checkGlErrors("applyNativeState(disabled)"); + } return; } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/msdf/WorldTextRenderer.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/msdf/WorldTextRenderer.java index 4077666..a8dddf2 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/msdf/WorldTextRenderer.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/text/msdf/WorldTextRenderer.java @@ -102,6 +102,22 @@ public enum WorldTextRenderer { return width * worldScale; } + /** Width the given text would occupy on screen, in the same world units as {@link Options#offsetX()}, without drawing anything. */ + public static double measureWidth(TextRenderer textRenderer, String text, Options options) { + if (text == null || text.isEmpty()) return 0.0; + Options resolved = options != null ? options : Options.defaults(); + + CustomTextRenderer custom = resolve(textRenderer); + if (custom == null) return 0.0; + + CustomTextRenderer.GlyphSelection selection = custom.prepareGlyphFont(resolved.scale(), resolved.big()); + GlyphFont font = selection.font(); + if (font == null) return 0.0; + + double glyphScale = selection.glyphScale(); + return font.getWidth(text, text.length()) * glyphScale * resolved.worldScale(); + } + private static void emitString(MeshBuilder mesh, GlyphFont font, String text, 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/SkyShaderStarryUniforms.java new file mode 100644 index 0000000..01172ff --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/engine/uniform/impl/sky/SkyShaderStarryUniforms.java @@ -0,0 +1,108 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.render.engine.uniform.impl.sky; + +import dev.loki.lovisual.render.engine.core.system.LoVisualRenderSystem; + +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; + +/** Uniform buffer for the SkyShader "Starry" fullscreen mode (star field + milky way + procedural round moon). */ +public enum SkyShaderStarryUniforms { + ; + public static final int SIZE = new Std140SizeCalculator() + .putVec4().putVec4().putVec4().putVec4().putVec4() + .putVec4().putVec4() + .get(); + + private static final Data DATA = new Data(); + private static final String UNIFORM_NAME = "LoVisual - SkyShaderStarry UBO"; + private static final int EXPECTED_WRITES_PER_FRAME = 4; + + public static void update(float width, float height, float time, float speed, + float intensity, float alpha, float fov, + float qx, float qy, float qz, float qw, + float moonDirX, float moonDirY, float moonDirZ, float moonSize, + float moonPhase01, float milkyWayIntensity, float starDensity, + float moonR, float moonG, float moonB, + float starR, float starG, float starB) { + DATA.resolution[0] = width; + DATA.resolution[1] = height; + DATA.resolution[2] = time; + DATA.resolution[3] = speed; + + DATA.settings[0] = intensity; + DATA.settings[1] = alpha; + DATA.settings[2] = fov; + DATA.settings[3] = starDensity; + + DATA.cameraDir[0] = qx; + DATA.cameraDir[1] = qy; + DATA.cameraDir[2] = qz; + DATA.cameraDir[3] = qw; + + DATA.moon[0] = moonDirX; + DATA.moon[1] = moonDirY; + DATA.moon[2] = moonDirZ; + DATA.moon[3] = moonSize; + + DATA.moonExtra[0] = moonPhase01; + DATA.moonExtra[1] = milkyWayIntensity; + DATA.moonExtra[2] = 0f; + DATA.moonExtra[3] = 0f; + + DATA.moonColor[0] = moonR; + DATA.moonColor[1] = moonG; + DATA.moonColor[2] = moonB; + DATA.moonColor[3] = 1f; + + DATA.starColor[0] = starR; + DATA.starColor[1] = starG; + DATA.starColor[2] = starB; + DATA.starColor[3] = 1f; + + LoVisualRenderSystem.uniforms().write(UNIFORM_NAME, SIZE, EXPECTED_WRITES_PER_FRAME, DATA); + } + + public static GpuBufferSlice get() { + return LoVisualRenderSystem.uniforms().current(UNIFORM_NAME); + } + + private static final class Data implements LoVisualUniformAllocator.UniformWriter { + private final float[] resolution = new float[4]; + private final float[] settings = new float[4]; + private final float[] cameraDir = new float[4]; + private final float[] moon = new float[4]; + private final float[] moonExtra = new float[4]; + private final float[] moonColor = new float[4]; + private final float[] starColor = new float[4]; + + @Override + public void write(java.nio.ByteBuffer buffer) { + Std140Builder builder = Std140Builder.intoBuffer(buffer); + putVec4(builder, resolution); + putVec4(builder, settings); + putVec4(builder, cameraDir); + putVec4(builder, moon); + putVec4(builder, moonExtra); + putVec4(builder, moonColor); + putVec4(builder, starColor); + } + + private static void putVec4(Std140Builder builder, float[] v) { + builder.putFloat(v[0]).putFloat(v[1]).putFloat(v[2]).putFloat(v[3]); + } + + @Override + public boolean equals(Object o) { + return false; + } + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/media/impl/linux/LinuxMediaSession.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/media/impl/linux/LinuxMediaSession.java index 93c21dc..d4dd7b5 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/media/impl/linux/LinuxMediaSession.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/media/impl/linux/LinuxMediaSession.java @@ -37,6 +37,7 @@ public final class LinuxMediaSession implements IMediaSession { private static final ConcurrentHashMap PENDING_TRACK_BY_OWNER = new ConcurrentHashMap<>(); private static final ConcurrentHashMap LAST_NUDGED_TRACK_ID_BY_OWNER = new ConcurrentHashMap<>(); private static final ConcurrentHashMap SEEK_OVERRIDE_BY_OWNER = new ConcurrentHashMap<>(); + private static final ConcurrentHashMap ARTWORK_CACHE_BY_URL = new ConcurrentHashMap<>(); private final Player dbus; private final String owner; @@ -379,14 +380,23 @@ public final class LinuxMediaSession implements IMediaSession { private byte[] readArtworkBytes(Map metadata) { Object raw = unwrap(metadata.get("mpris:artUrl")); if (!(raw instanceof String url) || url.isBlank()) return new byte[0]; + + byte[] cached = ARTWORK_CACHE_BY_URL.get(url); + if (cached != null) return cached; + try { URL connectionUrl = new URL(url); var connection = connectionUrl.openConnection(); - connection.setConnectTimeout(1500); - connection.setReadTimeout(1500); + connection.setConnectTimeout(3000); + connection.setReadTimeout(3000); + byte[] bytes; try (InputStream stream = connection.getInputStream()) { - return stream.readAllBytes(); + bytes = stream.readAllBytes(); } + if (bytes.length > 0) { + ARTWORK_CACHE_BY_URL.put(url, bytes); + } + return bytes; } catch (Exception ignored) { return new byte[0]; } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/media/session/MediaSessionService.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/media/session/MediaSessionService.java index 26331d3..9749d68 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/media/session/MediaSessionService.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/media/session/MediaSessionService.java @@ -266,9 +266,9 @@ public final class MediaSessionService { } boolean hasSession = session != null && media != null; - long predictedPosition = hasSession ? getPredictedPositionMs(media) / 1000L : 0L; + long predictedPositionMs = hasSession ? getPredictedPositionMs(media) : 0L; long duration = hasSession ? lastRawDuration : 0L; - snapshot = new Snapshot(mediaAvailable, session, media, artworkTexture, predictedPosition, duration, hasSession); + snapshot = new Snapshot(mediaAvailable, session, media, artworkTexture, predictedPositionMs, duration, hasSession); } private IMediaSession resolveSession() { @@ -351,6 +351,14 @@ public final class MediaSessionService { artworkTexture = ARTWORK_ID; } + // 300ms instead of 1s: MPRIS/SMTC position only updates when we ask for it (no + // PropertiesChanged push), so the raw sample is always stale by up to one poll + // interval. Polling faster shrinks that worst-case staleness window fourfold without + // adding network cost - these are local D-Bus/WinRT reads, not HTTP (artwork bytes are + // cached by URL, see LinuxMediaSession.readArtworkBytes, so this does not multiply + // network requests). + private static final long POLL_INTERVAL_MS = 300L; + private void pollMediaSessions() { while (pollingRunning) { try { @@ -361,7 +369,7 @@ public final class MediaSessionService { } try { - Thread.sleep(1000L); + Thread.sleep(POLL_INTERVAL_MS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); pollingRunning = false; @@ -545,13 +553,23 @@ public final class MediaSessionService { IMediaSession session, MediaInfo media, Identifier artworkTexture, - long predictedPositionSeconds, + long predictedPositionMs, long durationSeconds, boolean hasSession) { public static Snapshot empty(boolean mediaAvailable) { return new Snapshot(mediaAvailable, null, null, null, 0L, 0L, false); } + /** + * Whole-second position for UI widgets (mm:ss display, progress bars). Lyric sync must use + * {@link #predictedPositionMs()} directly instead — truncating to seconds here previously fed + * a staircase-quantized timestamp into KineticLyrics, even though the underlying prediction + * already extrapolates smoothly between MPRIS/SMTC polls. + */ + public long predictedPositionSeconds() { + return predictedPositionMs / 1000L; + } + public boolean isPlaying() { return media != null && media.playing(); } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/client/CooldownManager.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/client/CooldownManager.java index 1b5e25f..60e498a 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/client/CooldownManager.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/pvp/client/CooldownManager.java @@ -9,7 +9,6 @@ package dev.loki.lovisual.util.pvp.client; import net.minecraft.util.Util; import net.minecraft.world.item.Item; -import dev.loki.lovisual.config.i18n.MainConfig; import dev.loki.lovisual.config.values.collection.ItemCooldownRulesValue; import dev.loki.lovisual.util.pvp.cooldown.ItemCooldownSnapshot; import dev.loki.lovisual.util.pvp.cooldown.ItemUseCooldowns; @@ -38,7 +37,7 @@ public class CooldownManager { } public boolean isInPvp() { - return MainConfig.get().isForcePvp() || inPvp; + return inPvp; } public boolean isPvpGraceActive(long graceMs) { diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/lang/en_us.json b/combatant-client-26.2/src/main/resources/assets/lovisual/lang/en_us.json index 9dabdc1..41b2d3f 100644 --- a/combatant-client-26.2/src/main/resources/assets/lovisual/lang/en_us.json +++ b/combatant-client-26.2/src/main/resources/assets/lovisual/lang/en_us.json @@ -166,7 +166,6 @@ "setting.main_config.debug.option.error_and_warnings": "Errors and warnings", "setting.main_config.debug.option.serverdebug": "Server debug", "setting.main_config.debug.option.all": "All", - "setting.main_config.forcePvp": "Force PvP", "setting.main_config.menuBg": "Menu background", "setting.main_config.menuBg.option.off": "Off", "setting.main_config.menuBg.option.waves": "Waves", @@ -1141,7 +1140,7 @@ "setting.postfx.vignette_radius": "Vignette radius", "setting.timecontrol.sync_system_time": "Sync with system time", "setting.autoleave.respect_pvp": "Respect PVP", - "setting.autoleave.hp_threshold": "HP threshold", + "setting.autoleave.hp_threshold": "Health threshold", "setting.shitdropper.max_trash_slots": "Max slots", "setting.shitdropper.delay_ticks": "Delay (ticks)", "setting.shitdropper.trash_item_ids": "Trash item ids", @@ -1306,18 +1305,6 @@ "setting.esp.entity_color": "Entity Color", "setting.esp.search_entities": "Search Entities", "setting.esp.render_self": "Render Self", - "setting.tazikhat.render_target": "Render", - "setting.tazikhat.render_target.option.self": "Self", - "setting.tazikhat.render_target.option.players": "Players", - "setting.tazikhat.render_target.option.both": "Both", - "setting.tazikhat.rim_color": "Rim color", - "setting.tazikhat.helmet_scale": "Helmet scale", - "setting.tazikhat.head_top_offset": "Head height", - "setting.tazikhat.head_forward_offset": "Forward offset", - "setting.tazikhat.head_right_offset": "Right offset", - "setting.tazikhat.wobble_strength": "Wobble strength", - "setting.tazikhat.death_flight": "Death flight", - "setting.tazikhat.death_flight_power": "Death flight power", "setting.cameraclip.distance_multiplier": "Distance multiplier", "setting.cameraclip.back_offset": "Back offset", "setting.cameraclip.right_offset": "Right offset", @@ -3256,18 +3243,6 @@ "command.tps.description": "Shows estimated server ticks per second.", "command.username.description": "Shows the current Minecraft username and UUID.", "command.xaero.description": "Adds, lists, or removes Xaero waypoints.", - "module.chinahat.description": "Renders a classic conical Chinese hat on players' heads.", - "setting.chinahat.render_target": "Render", - "setting.chinahat.render_target.option.self": "Self", - "setting.chinahat.render_target.option.players": "Players", - "setting.chinahat.render_target.option.both": "Both", - "setting.chinahat.rim_color": "Rim color", - "setting.chinahat.segments": "Segments", - "setting.chinahat.brim_radius": "Brim radius", - "setting.chinahat.cone_height": "Cone height", - "setting.chinahat.head_top_offset": "Head height", - "setting.chinahat.head_forward_offset": "Forward offset", - "setting.chinahat.head_right_offset": "Right offset", "module.animations.description": "Animates interface transitions.", "setting.animations.hotbar_raise": "Hotbar raise", "setting.animations.inventory": "Inventory open", @@ -3309,7 +3284,6 @@ "setting.holdmyitems.join_appear": "Smooth world entry", "module.autoleave.description": "Leaves the server when your health drops low or unfriendly players come nearby.", "setting.autoleave.hp_enabled": "Leave on low health", - "setting.autoleave.hp_threshold": "Health threshold", "setting.autoleave.players_enabled": "Leave when players nearby", "setting.autoleave.player_count": "Player count", "setting.autoleave.radius": "Radius", @@ -3343,5 +3317,76 @@ "setting.streamermode.mask_server_words": "Mask server words", "setting.streamermode.hide_skins": "Hide skins", "module.itemscroller.description": "Shift-moves items with the mouse wheel inside any container.", - "setting.itemscroller.hotbar_fallback": "Scroll moves hotbar slot" + "setting.itemscroller.hotbar_fallback": "Scroll moves hotbar slot", + "setting.tazikhat.render_target": "Render", + "setting.tazikhat.render_target.option.self": "Self", + "setting.tazikhat.render_target.option.players": "Players", + "setting.tazikhat.render_target.option.both": "Both", + "setting.tazikhat.rim_color": "Rim color", + "setting.tazikhat.helmet_scale": "Helmet scale", + "setting.tazikhat.head_top_offset": "Head height", + "setting.tazikhat.head_forward_offset": "Forward offset", + "setting.tazikhat.head_right_offset": "Right offset", + "setting.tazikhat.wobble_strength": "Wobble strength", + "module.chinahat.description": "Renders a classic conical Chinese hat on players' heads.", + "setting.chinahat.render_target": "Render", + "setting.chinahat.render_target.option.self": "Self", + "setting.chinahat.render_target.option.players": "Players", + "setting.chinahat.render_target.option.both": "Both", + "setting.chinahat.rim_color": "Rim color", + "setting.chinahat.segments": "Segments", + "setting.chinahat.brim_radius": "Brim radius", + "setting.chinahat.cone_height": "Cone height", + "setting.chinahat.head_top_offset": "Head height", + "setting.chinahat.head_forward_offset": "Forward offset", + "setting.chinahat.head_right_offset": "Right offset", + "module.fpsoptimizer.name": "FPS Optimizer", + "module.fpsoptimizer.description": "Manual toggles to cut render cost: vanilla particles, menu background shader, glass/blur, bloom/glow, off-screen entity culling.", + "setting.fpsoptimizer.disable_particles": "Disable particles", + "setting.fpsoptimizer.disable_menu_background": "Disable menu background", + "setting.fpsoptimizer.disable_glass": "Disable glass/blur effects", + "setting.fpsoptimizer.disable_bloom": "Disable bloom/glow effects", + "setting.fpsoptimizer.entity_culling": "Cull off-screen entities", + "setting.fpsoptimizer.entity_cull_distance": "Entity cull distance", + "setting.cosmeticdisplay.next_cosmetic.desc": "Cycles to the next model in the Model slot.", + "setting.cosmeticdisplay.prev_cosmetic.desc": "Cycles to the previous model in the Model slot.", + "setting.cosmeticdisplay.open_cosmetics.desc": "Opens the cosmetics editor: 3D preview with clickable body zones and per-slot model pickers.", + "setting.zoom.zoom_hold.desc": "Hold to zoom the camera in.", + "setting.thirdperson.look.desc": "Hold to orbit the camera around your player in free third-person view.", + "setting.freecam.toggle_input.desc": "Toggles whether WASD input still moves the freecam.", + "setting.definetarget.toggle_target.desc": "Toggles defining the entity you look at as your target.", + "setting.shadersky.mode.option.starry": "Starry", + "setting.shadersky.starry_intensity": "Star intensity", + "setting.shadersky.starry_density": "Star density", + "setting.shadersky.starry_milkyway": "Milky way", + "setting.shadersky.starry_moon": "Show moon", + "setting.shadersky.starry_moon_size": "Moon size", + "setting.shadersky.starry_moon_color": "Moon color", + "setting.shadersky.starry_star_color": "Star color", + "module.wings.name": "Wings", + "module.wings.description": "Cosmetic wings on players' backs: 8 silhouette types with pose-driven flapping.", + "setting.wings.type": "Wing type", + "setting.wings.type.option.angelic": "Angelic", + "setting.wings.type.option.dragon": "Dragon", + "setting.wings.type.option.butterfly": "Butterfly", + "setting.wings.type.option.phoenix": "Phoenix", + "setting.wings.type.option.crystal": "Crystal", + "setting.wings.type.option.mechanical": "Mechanical", + "setting.wings.type.option.fairy": "Fairy", + "setting.wings.type.option.demon": "Demon", + "setting.wings.target": "Targets", + "setting.wings.target.option.self": "Self", + "setting.wings.target.option.friends": "Friends", + "setting.wings.target.option.all_players": "All players", + "setting.wings.scale": "Scale", + "setting.wings.flapping": "Flapping", + "setting.wings.flap_strength": "Flap strength", + "setting.wings.flap_speed": "Flap speed", + "setting.wings.through_walls": "Through walls", + "setting.wings.glow": "Glow layers", + "setting.wings.outline": "Outline", + "setting.wings.ribs": "Ribs", + "setting.wings.color": "Color", + "setting.kineticlyrics.time_offset_ms.desc": "Shifts lyric timing vs the audible audio (ms). Positive = text later. Player position leads the speakers (Bluetooth: 0.5-1s+), so a negative default compensates. Tune with debug mode if it still drifts.", + "setting.kineticlyrics.arc_spread.desc": "How wide around your view lines may spawn; automatically clamped to your real horizontal FOV." } diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/lang/ru_ru.json b/combatant-client-26.2/src/main/resources/assets/lovisual/lang/ru_ru.json index 0864e8f..38daabb 100644 --- a/combatant-client-26.2/src/main/resources/assets/lovisual/lang/ru_ru.json +++ b/combatant-client-26.2/src/main/resources/assets/lovisual/lang/ru_ru.json @@ -1082,7 +1082,7 @@ "setting.postfx.vignette_radius": "Радиус виньетки", "setting.timecontrol.sync_system_time": "Синхронизировать с временем системы", "setting.autoleave.respect_pvp": "Уважать PVP", - "setting.autoleave.hp_threshold": "Порог ХП", + "setting.autoleave.hp_threshold": "Порог здоровья", "setting.shitdropper.max_trash_slots": "Макс. слотов", "setting.shitdropper.delay_ticks": "Задержка (тики)", "setting.shitdropper.trash_item_ids": "Id мусорных предметов", @@ -1104,14 +1104,6 @@ "setting.esp.entity_color": "Цвет сущностей", "setting.esp.search_entities": "Поиск сущностей", "setting.esp.render_self": "Отображать себя", - "setting.tazikhat.rim_color": "Цвет обода", - "setting.tazikhat.helmet_scale": "Размер со шлемом", - "setting.tazikhat.head_top_offset": "Высота на голове", - "setting.tazikhat.head_forward_offset": "Смещение вперёд", - "setting.tazikhat.head_right_offset": "Смещение вправо", - "setting.tazikhat.wobble_strength": "Сила колыхания", - "setting.tazikhat.death_flight": "Улёт при смерти", - "setting.tazikhat.death_flight_power": "Сила улёта", "setting.cameraclip.distance_multiplier": "Множитель дистанции", "setting.cameraclip.back_offset": "Смещение назад", "setting.cameraclip.right_offset": "Смещение вправо", @@ -3046,18 +3038,6 @@ "command.tps.description": "Показывает расчётное количество серверных тиков в секунду.", "command.username.description": "Показывает текущее имя Minecraft и UUID.", "command.xaero.description": "Добавляет, показывает или удаляет точки Xaero.", - "module.chinahat.description": "Рисует классическую коническую китайскую шляпу на головах игроков.", - "setting.chinahat.render_target": "Рендер", - "setting.chinahat.render_target.option.self": "Себе", - "setting.chinahat.render_target.option.players": "Игрокам", - "setting.chinahat.render_target.option.both": "Всем", - "setting.chinahat.rim_color": "Цвет обода", - "setting.chinahat.segments": "Сегменты", - "setting.chinahat.brim_radius": "Радиус полей", - "setting.chinahat.cone_height": "Высота конуса", - "setting.chinahat.head_top_offset": "Высота на голове", - "setting.chinahat.head_forward_offset": "Смещение вперёд", - "setting.chinahat.head_right_offset": "Смещение вправо", "module.animations.description": "Плавные анимации интерфейса.", "setting.animations.hotbar_raise": "Подъём хотбара", "setting.animations.inventory": "Открытие инвентаря", @@ -3099,7 +3079,6 @@ "setting.holdmyitems.join_appear": "Плавное появление при входе", "module.autoleave.description": "Покидает сервер при низком здоровье или когда рядом оказываются недруги.", "setting.autoleave.hp_enabled": "Выход при низком ХП", - "setting.autoleave.hp_threshold": "Порог здоровья", "setting.autoleave.players_enabled": "Выход при игроках рядом", "setting.autoleave.player_count": "Количество игроков", "setting.autoleave.radius": "Радиус", @@ -3133,5 +3112,72 @@ "setting.streamermode.mask_server_words": "Маскировать названия серверов", "setting.streamermode.hide_skins": "Скрывать скины", "module.itemscroller.description": "Перекладывает предметы колёсиком мыши (shift-клик) в любом инвентаре.", - "setting.itemscroller.hotbar_fallback": "Скролл двигает слот хотбара" + "setting.itemscroller.hotbar_fallback": "Скролл двигает слот хотбара", + "setting.tazikhat.rim_color": "Цвет обода", + "setting.tazikhat.helmet_scale": "Размер со шлемом", + "setting.tazikhat.head_top_offset": "Высота на голове", + "setting.tazikhat.head_forward_offset": "Смещение вперёд", + "setting.tazikhat.head_right_offset": "Смещение вправо", + "setting.tazikhat.wobble_strength": "Сила колыхания", + "module.chinahat.description": "Рисует классическую коническую китайскую шляпу на головах игроков.", + "setting.chinahat.render_target": "Рендер", + "setting.chinahat.render_target.option.self": "Себе", + "setting.chinahat.render_target.option.players": "Игрокам", + "setting.chinahat.render_target.option.both": "Всем", + "setting.chinahat.rim_color": "Цвет обода", + "setting.chinahat.segments": "Сегменты", + "setting.chinahat.brim_radius": "Радиус полей", + "setting.chinahat.cone_height": "Высота конуса", + "setting.chinahat.head_top_offset": "Высота на голове", + "setting.chinahat.head_forward_offset": "Смещение вперёд", + "setting.chinahat.head_right_offset": "Смещение вправо", + "module.fpsoptimizer.name": "Оптимизация FPS", + "module.fpsoptimizer.description": "Ручные тумблеры для снижения нагрузки рендера: ванильные частицы, фоновый шейдер меню, стекло/блюр, блум/свечение, отсечение сущностей за экраном.", + "setting.fpsoptimizer.disable_particles": "Отключить частицы", + "setting.fpsoptimizer.disable_menu_background": "Отключить фон меню", + "setting.fpsoptimizer.disable_glass": "Отключить стекло/блюр", + "setting.fpsoptimizer.disable_bloom": "Отключить блум/свечение", + "setting.fpsoptimizer.entity_culling": "Скрывать сущности за экраном", + "setting.fpsoptimizer.entity_cull_distance": "Дистанция отсечения сущностей", + "setting.cosmeticdisplay.next_cosmetic.desc": "Следующая модель в слоте «Модель».", + "setting.cosmeticdisplay.prev_cosmetic.desc": "Предыдущая модель в слоте «Модель».", + "setting.cosmeticdisplay.open_cosmetics.desc": "Открывает редактор косметики: 3D-превью с кликабельными зонами тела и выбором моделей по слотам.", + "setting.zoom.zoom_hold.desc": "Удерживай, чтобы приблизить камеру.", + "setting.thirdperson.look.desc": "Удерживай, чтобы вращать камеру вокруг игрока (обзор от третьего лица).", + "setting.freecam.toggle_input.desc": "Вкл/выкл движение WASD в полётном режиме камеры.", + "setting.definetarget.toggle_target.desc": "Отмечает сущность, на которую смотришь, как цель.", + "setting.shadersky.mode.option.starry": "Звёздное", + "setting.shadersky.starry_intensity": "Яркость звёзд", + "setting.shadersky.starry_density": "Плотность звёзд", + "setting.shadersky.starry_milkyway": "Млечный путь", + "setting.shadersky.starry_moon": "Показывать луну", + "setting.shadersky.starry_moon_size": "Размер луны", + "setting.shadersky.starry_moon_color": "Цвет луны", + "setting.shadersky.starry_star_color": "Цвет звёзд", + "module.wings.name": "Крылья", + "module.wings.description": "Косметические крылья на спине игроков: 8 типов формы с анимацией взмахов.", + "setting.wings.type": "Тип крыльев", + "setting.wings.type.option.angelic": "Ангельские", + "setting.wings.type.option.dragon": "Драконьи", + "setting.wings.type.option.butterfly": "Бабочка", + "setting.wings.type.option.phoenix": "Феникс", + "setting.wings.type.option.crystal": "Кристалл", + "setting.wings.type.option.mechanical": "Механические", + "setting.wings.type.option.fairy": "Фея", + "setting.wings.type.option.demon": "Демонические", + "setting.wings.target": "Кому показывать", + "setting.wings.target.option.self": "Только себе", + "setting.wings.target.option.friends": "Друзьям", + "setting.wings.target.option.all_players": "Всем игрокам", + "setting.wings.scale": "Масштаб", + "setting.wings.flapping": "Взмахи", + "setting.wings.flap_strength": "Сила взмаха", + "setting.wings.flap_speed": "Скорость взмаха", + "setting.wings.through_walls": "Сквозь стены", + "setting.wings.glow": "Слои свечения", + "setting.wings.outline": "Обводка", + "setting.wings.ribs": "Перья (рёбра)", + "setting.wings.color": "Цвет", + "setting.kineticlyrics.time_offset_ms.desc": "Сдвиг тайминга текста относительно слышимого звука (мс). Плюс = текст позже. Позиция плеера опережает динамики (Bluetooth: 0.5–1с+), дефолтный минус это компенсирует. Подстрой через debug, если всё ещё плывёт.", + "setting.kineticlyrics.arc_spread.desc": "Насколько широко вокруг обзора могут появляться строки; автоматически ограничивается реальным горизонтальным FOV." } diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/sky_shader_starry.frag b/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/sky_shader_starry.frag new file mode 100644 index 0000000..bc88694 --- /dev/null +++ b/combatant-client-26.2/src/main/resources/assets/lovisual/shaders/sky_shader_starry.frag @@ -0,0 +1,159 @@ +#version 330 core + +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + */ + +out vec4 color; + +layout (std140) uniform SkyShaderStarry { + vec4 u_Resolution; // xy = size, z = time, w = speed + vec4 u_Settings; // x = intensity, y = alpha, z = FOV, w = starDensity + vec4 u_CameraDir; // orientation quaternion + vec4 u_Moon; // xyz = world-space moon direction, w = angular radius + vec4 u_MoonExtra; // x = moon phase (0..1), y = milky way intensity, zw = spare + vec4 u_MoonColor; + vec4 u_StarColor; +}; + +float hash13(vec3 p) { + p = fract(p * 0.3183099 + vec3(0.1, 0.2, 0.3)); + p *= 17.0; + return fract(p.x * p.y * p.z * (p.x + p.y + p.z)); +} + +float hash21(vec2 p) { + p = fract(p * vec2(443.8975, 397.2973)); + p += dot(p, p + 19.19); + return fract(p.x * p.y); +} + +float smoothNoise2D(vec2 p) { + vec2 i = floor(p); + vec2 f = fract(p); + f = f * f * (3.0 - 2.0 * f); + float a = hash21(i); + float b = hash21(i + vec2(1.0, 0.0)); + float c = hash21(i + vec2(0.0, 1.0)); + float d = hash21(i + vec2(1.0, 1.0)); + return mix(mix(a, b, f.x), mix(c, d, f.x), f.y); +} + +float fbm(vec2 p) { + float v = 0.0, a = 0.5, freq = 1.0; + for (int k = 0; k < 4; k++) { + v += a * smoothNoise2D(p * freq); + freq *= 2.05; + a *= 0.5; + } + return v; +} + +vec3 calcRayDir() { + vec2 uv = gl_FragCoord.xy / u_Resolution.xy; + vec2 sp = uv * 2.0 - 1.0; + float aspect = u_Resolution.x / u_Resolution.y; + float tanV = tan(radians(u_Settings.z) * 0.5); + vec3 ray = normalize(vec3(sp.x * tanV * aspect, sp.y * tanV, -1.0)); + + vec3 qv = u_CameraDir.xyz; + float qw = u_CameraDir.w; + mat3 rot = mat3( + 1.0 - 2.0*(qv.y*qv.y + qv.z*qv.z), 2.0*(qv.x*qv.y + qv.z*qw), 2.0*(qv.x*qv.z - qv.y*qw), + 2.0*(qv.x*qv.y - qv.z*qw), 1.0 - 2.0*(qv.x*qv.x + qv.z*qv.z), 2.0*(qv.y*qv.z + qv.x*qw), + 2.0*(qv.x*qv.z + qv.y*qw), 2.0*(qv.y*qv.z - qv.x*qw), 1.0 - 2.0*(qv.x*qv.x + qv.y*qv.y) + ); + return rot * ray; +} + +float starField(vec3 dir, float time, float density) { + vec3 grid = dir * density; + vec3 cell = floor(grid); + float h = hash13(cell); + float exists = step(0.986, h); + float twinkle = 0.6 + 0.4 * sin(time * (2.0 + h * 6.0) + h * 43.0); + float brightness = fract(h * 91.7) * 0.7 + 0.3; + return exists * twinkle * brightness; +} + +float milkyWayBand(vec3 dir, float time) { + vec3 axis = normalize(vec3(0.35, 0.12, 0.93)); + float t = dot(dir, axis); + float band = exp(-t * t * 7.0); + vec2 texCoord = vec2(dir.x - dir.z, dir.y) * 2.5 + time * 0.005; + float texture = fbm(texCoord) * 0.6 + fbm(texCoord * 2.3 + 5.0) * 0.4; + return band * (0.55 + 0.45 * texture); +} + +vec3 basisFromDir(vec3 dir, out vec3 right, out vec3 up) { + vec3 worldUp = abs(dir.y) > 0.99 ? vec3(1.0, 0.0, 0.0) : vec3(0.0, 1.0, 0.0); + right = normalize(cross(worldUp, dir)); + up = normalize(cross(dir, right)); + return dir; +} + +float moonDisc(vec3 dir, out float lit) { + vec3 moonDir = normalize(u_Moon.xyz); + float angularRadius = max(0.002, u_Moon.w); + + vec3 right, up; + basisFromDir(moonDir, right, up); + + vec3 diff = dir - moonDir; + vec2 p = vec2(dot(diff, right), dot(diff, up)) / angularRadius; + + float edge = 0.06; + float distMoon = length(p); + float moonMask = 1.0 - smoothstep(1.0 - edge, 1.0 + edge, distMoon); + if (moonMask <= 0.001) { + lit = 0.0; + return 0.0; + } + + float phaseAngle = fract(u_MoonExtra.x) * 6.2831853; + float side = sin(phaseAngle) >= 0.0 ? 1.0 : -1.0; + float shadowX = side * (1.0 - cos(phaseAngle)); + + float distShadow = length(p - vec2(shadowX, 0.0)); + float shadowMask = 1.0 - smoothstep(1.0 - edge, 1.0 + edge, distShadow); + + float limbDarkening = 1.0 - 0.25 * distMoon * distMoon; + float litArea = moonMask * (1.0 - shadowMask) * limbDarkening; + float earthshine = moonMask * shadowMask * 0.10; + + lit = litArea + earthshine; + return moonMask; +} + +void main() { + vec3 dir = normalize(calcRayDir()); + float time = u_Resolution.z; + float speed = u_Resolution.w; + float anim = time * speed; + + float horizonFade = smoothstep(-0.05, 0.12, dir.y); + if (horizonFade < 0.005) discard; + + float intensity = max(0.0001, u_Settings.x); + float alpha = u_Settings.y; + float starDensity = max(1.0, u_Settings.w); + float milkyWayIntensity = u_MoonExtra.y; + + vec3 skyBase = vec3(0.015, 0.02, 0.05); + + float stars = starField(dir, anim, starDensity); + float milkyWay = milkyWayBand(dir, anim) * milkyWayIntensity; + + float moonLit; + float moonMask = moonDisc(dir, moonLit); + + vec3 skyColor = skyBase; + skyColor += u_StarColor.rgb * stars * intensity; + skyColor += mix(u_StarColor.rgb, vec3(0.75, 0.8, 1.0), 0.4) * milkyWay * intensity; + skyColor = mix(skyColor, u_MoonColor.rgb * (0.4 + 0.6 * moonLit), moonMask); + skyColor += u_MoonColor.rgb * moonLit * 0.6 * intensity; + + float finalAlpha = alpha * horizonFade; + color = vec4(clamp(skyColor, 0.0, 1.0), finalAlpha); +} 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 8de260b..1b3e2dc 100644 --- a/combatant-client-26.2/src/main/resources/lovisual.mixins.json +++ b/combatant-client-26.2/src/main/resources/lovisual.mixins.json @@ -140,6 +140,7 @@ "network.game.PlayerInfoMixin", "network.packet.ClientCommonPacketListenerImplMixin", "network.packet.ServerboundUseItemPacketMixin", + "render.entity.EntityRenderDispatcherMixin", "render.entity.ItemEntityRenderStateMixin", "render.entity.ItemEntityRendererMixin", "render.entity.LivingEntityRenderStateMixin", diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/ChinaHatPaletteTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/ChinaHatPaletteTest.java index 85de213..e0b32f0 100644 --- a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/ChinaHatPaletteTest.java +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/ChinaHatPaletteTest.java @@ -10,7 +10,6 @@ package dev.loki.lovisual.features.module.modules.visuals; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; -import dev.loki.lovisual.features.module.modules.visuals.hats.ChinaHat; import dev.loki.lovisual.features.module.modules.visuals.hats.ChinaHatPalette; public final class ChinaHatPaletteTest { @@ -67,25 +66,4 @@ public final class ChinaHatPaletteTest { assertEquals(b, ChinaHatPalette.mix(a, b, 2.0f)); assertEquals(0x80808080, ChinaHatPalette.mix(a, b, 0.5f)); } - - @Test - void colorModeIdsAreUniqueAndLowercase() { - String[] ids = java.util.Arrays.stream(ChinaHat.ColorMode.values()) - .map(ChinaHat.ColorMode::id) - .toArray(String[]::new); - - assertEquals(ChinaHat.ColorMode.values().length, java.util.Arrays.stream(ids).distinct().count()); - for (String id : ids) { - assertEquals(id, id.toLowerCase()); - } - } - - @Test - void renderTargetIdsAreUnique() { - String[] ids = java.util.Arrays.stream(ChinaHat.RenderTarget.values()) - .map(ChinaHat.RenderTarget::id) - .toArray(String[]::new); - - assertEquals(ChinaHat.RenderTarget.values().length, java.util.Arrays.stream(ids).distinct().count()); - } } diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/cosmetics/CosmeticsHotspotTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/cosmetics/CosmeticsHotspotTest.java deleted file mode 100644 index 1fb4030..0000000 --- a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/cosmetics/CosmeticsHotspotTest.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * This file is part of the LoVisual Client distribution. - * Copyright (c) 2026 loki5512344. - * - * Licensed under the GNU General Public License v3.0. - */ - -package dev.loki.lovisual.features.module.modules.visuals.cosmetics; - -import dev.loki.lovisual.features.cosmetic.model.CosmeticSlot; -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.module.modules.visuals.cosmetics.CosmeticsHotspot; - -public final class CosmeticsHotspotTest { - - @Test - void headIsTopZone() { - CosmeticsHotspot[] zones = CosmeticsHotspot.forRegion(0f, 0f, 100f, 100f, false); - assertEquals(CosmeticSlot.HEAD, CosmeticsHotspot.hitTest(zones, 50, 5)); - } - - @Test - void legsIsBottomZone() { - CosmeticsHotspot[] zones = CosmeticsHotspot.forRegion(0f, 0f, 100f, 100f, false); - assertEquals(CosmeticSlot.LEGS, CosmeticsHotspot.hitTest(zones, 50, 95)); - } - - @Test - void shouldersZoneBecomesBackWhenBackViewToggled() { - CosmeticsHotspot[] front = CosmeticsHotspot.forRegion(0f, 0f, 100f, 100f, false); - CosmeticsHotspot[] back = CosmeticsHotspot.forRegion(0f, 0f, 100f, 100f, true); - - assertEquals(CosmeticSlot.SHOULDERS, CosmeticsHotspot.hitTest(front, 50, 25)); - assertEquals(CosmeticSlot.BACK, CosmeticsHotspot.hitTest(back, 50, 25)); - } - - @Test - void outsideRegionMissesAllZones() { - CosmeticsHotspot[] zones = CosmeticsHotspot.forRegion(10f, 10f, 100f, 100f, false); - assertNull(CosmeticsHotspot.hitTest(zones, 5, 5)); - } -} diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/wings/WingShapesTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/wings/WingShapesTest.java new file mode 100644 index 0000000..c53da13 --- /dev/null +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/wings/WingShapesTest.java @@ -0,0 +1,57 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.visuals.wings; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public final class WingShapesTest { + + @Test + void everyTypeResolvesToNonEmptyShape() { + for (String type : WingShapes.TYPES) { + WingShapes.Point[] shape = WingShapes.shape(type); + assertNotNull(shape, type); + assertTrue(shape.length >= 10, type); + } + // unknown type falls back to Angelic + assertSame(WingShapes.shape("Angelic"), WingShapes.shape("Nope")); + } + + @Test + void allShapesUseFullPointRanges() { + for (String type : WingShapes.TYPES) { + for (WingShapes.Point p : WingShapes.shape(type)) { + assertTrue(p.alphaMul() > 0f && p.alphaMul() <= 1f, type + " " + p); + assertTrue(p.x() >= 0f, type + " " + p); + } + } + } + + @Test + void ribIndicesFitShape() { + for (String type : WingShapes.TYPES) { + WingShapes.Point[] shape = WingShapes.shape(type); + for (int idx : WingShapes.ribs(shape)) { + assertTrue(idx < shape.length, type + " rib " + idx); + } + } + } + + @Test + void glidePoseScalesPitchWithProgress() { + WingShapes.Pose start = WingShapes.glidePose(0f, 0f); + WingShapes.Pose end = WingShapes.glidePose(0f, 1f); + assertEquals(0f, start.pitchRotation(), 0.001f); + assertEquals(-90f, end.pitchRotation(), 0.001f); + } +}