diff --git a/TODO.md b/TODO.md index bad0021..27d73b2 100644 --- a/TODO.md +++ b/TODO.md @@ -27,4 +27,45 @@ - [x] Фаза 3.5: Удаление читерских модулей (легит-направление) - [x] Фаза 4: Security (BackdoorProtection, StaffTracker) - [x] Фаза 5: Нормализация модулей (ленивый Minecraft, GameClock) -- [ ] Фаза 6: Тесты (EventBus, конфиг, утилиты) — см. `combatant-client-26.2/TODO.md` +- [x] Фаза 6: Тесты (EventBus, конфиг, утилиты) — см. `combatant-client-26.2/TODO.md` +- [ ] Фаза 7: Новые модули по мотивам SoupVisuals — см. ниже + +--- + +## Фаза 7: Новые модули — референс SoupVisuals +> Референс: `ref/soupvisuals-src/` — SoupVisuals 3.2.0 (MC 1.21.11, лицензия SOUP-1.0). +> Код референса НЕ копируем (чужая лицензия + декомпил) — только пере-реализация идей +> под наши правила (≤200 строк/файл, ≤4 файла/папку, без статики, тесты). +> Модули референса лежат в `ref/soupvisuals-src/padej/soup/implement/features/modules/`. + +- [x] **Hitboxes** (visuals) — цветные хитбоксы сущностей, как у Soup: ТОЛЬКО рендер + контейнера с настройкой цвета/видимости, БЕЗ увеличения хитбокса (увеличение — + чит, удалён в Фазе 3.5). Референс: `visuals/Hitboxes.java` +- [x] **Watermark** (HUD, draggable) — обещан в README, в коде отсутствует +- [ ] **Gps** (world) — луч/маркер направления к точке (у Soup ~400 стр.); связка + с Xaero-waypoints и координатами. Референс: `world/Gps.java` +- [x] **Light** (world) — кастомный цвет освещения (у Soup 41 стр.). Референс: `world/Light.java` +- [ ] **HitBubbles** (particles) — пузыри при ударах по игрокам (у Soup 323 стр.). + Референс: `particles/HitBubbles.java` +- [ ] **AmbientParticles + FireFlyParticle** (particles) — атмосферные частицы мира / + светлячки (у Soup 775 + 241 стр.). Референс: `particles/AmbientParticles.java`, + `particles/types/FireFlyParticle.java` +- [x] **NameProtect** (misc) — подмена своего ника и ников друзей в тексте чата/скорборда + (у Soup 42 стр.). Референс: `other/NameProtect.java` +- [ ] **EndCrystal** (visuals) — улучшенный рендер энд-кристаллов (у Soup 217 стр.). + Референс: `other/EndCrystal.java` +- [ ] **Trinket 2.0** — апгрейд нашего CosmeticDisplay до уровня Soup Trinket: + иконки-спутники (Atom/Ball/Cat/Donut/Flame/Flower/Gem/Ghost/MCTiers), трейл + (цвет sync/custom, длина, толщина), звуки (типы + громкость), частицы + (у Soup 315 стр.). Референс: `other/Trinket.java` +- [ ] **Balancer** (mctiers) — тиры игроков с mctiers.info (у Soup отдельная категория + MCTiers). Референс: `mctiers/Balancer.java` +- [ ] **HandsShader: новые шейдеры руки** — у Soup 10 режимов (Solid, Chroma, Balatro, + Smoke, Stripes, Glow, Glass, Invert, Snow) + dual-режим с blend-режимами + (Mix/Add/Multiply/Screen/Overlay/Difference); шейдеры лежат в + `ref/soupvisuals-src/assets/soupapi/shaders/hand/`. Пере-реализовать недостающие + и добавить к нашим (BackSword/Chams hand shaders уже есть dual — сверить набор). + Референс: `visuals/HandsShader.java` +- [x] **Фикс ChinaHat** — текущая геометрия кривая (смахивает на самбреро/зонт); + переделать модель в нормальную конусную шляпу по референсу Soup + (`visuals/ChinaHat.java` + `ChinaHatModel.java`). diff --git a/combatant-client-26.2/DEVELOPMENT.md b/combatant-client-26.2/DEVELOPMENT.md new file mode 100644 index 0000000..405f1c8 --- /dev/null +++ b/combatant-client-26.2/DEVELOPMENT.md @@ -0,0 +1,200 @@ +# LoVisual — руководство разработчика + +Как устроен проект, как добавлять фичи и какие правила обязательны. +Версии: **Java 25 · Fabric Loader 0.19.3 · Minecraft 26.2** (Loom). + +--- + +## 1. Сборка и запуск + +```bash +cd combatant-client-26.2 +./gradlew build # jar: build/libs/lovisual-.jar +./gradlew test # юнит-тесты (сейчас ~320) +./gradlew runClient # быстрый dev-запуск Minecraft с модом +./gradlew runClient --rerun-tasks # если кэш врёт +``` + +Готовый jar для теста в реальной сборке — скопировать в инстанс: + +```bash +cp build/libs/lovisual-*.jar /storage/prismlauncher/instances/26.2/minecraft/mods/ +``` + +Ресурсы (текстуры) — **только настоящие PNG** (сигнатура `89 50 4E 47`). +JPEG, переименованный в .png, уронит `Bad PNG Signature` при загрузке ресурспака. + +--- + +## 2. Правила проекта (нарушение = откат) + +- **KISS / DRY / SOLID** — простая структура, без оверинжиниринга. +- **≤ 200 строк на файл.** Фасад-делегат (например `Renderer2D`) — исключение. + Больше логики — выноси package-private хелперы в тот же пакет. +- **≤ 4 файла на папку** — больше, дели папку по смыслу (подпапка). +- **Никакого статики от Minecraft**: `Minecraft.getInstance()` — только локальной + переменной в месте использования, НИКОГДА в поле (`private final Minecraft mc = ...` — баг). + Функциональный синглтон (`INSTANCE`) — только для сервисов без состояния жизненного цикла. +- **`Lifecycle.isActive()/canRunHud()` — гейты** для рендера/событий; `EventBus.post()` + сам игнорирует события до activation. +- **Никаких this-escape** — конструктор не публикует `this`. +- **Никаких мёртвых миксинов** — миксин только если реально нужен; + `cancellable=true` только при реальном cancel. +- **`dev.loki.lovisual.mixins.*` — ТОЛЬКО классы с `@Mixin`.** Обычный helper-класс + внутри mixin-пакета крашит игру: Knot не даёт transformed-классу обратиться к нему + (`IllegalClassLoadError`). Хелперы — в `util/` или пакет фичи (см. `HandDynamics`). +- **Без пустышек** — файл без реализации не создаётся. +- **Каждый этап компилируется**: `./gradlew build` перед коммитом. +- **Тесты обязательны** для новой чистой логики (JUnit 5, без моков Minecraft — + выноси математику/парсинг в чистые статические методы и тесть их). +- **Референсы (`ref/`)** — чужой код (SoupVisuals SOUP-1.0, Delta и др.): + **только идеи**, пере-реализация под наши правила, никакого копирования. + +--- + +## 3. Карта пакетов + +``` +dev.loki.lovisual +├── LoVisual.java — инициализация ClientModInitializer +├── Lifecycle.java — INIT → ACTIVE → SHUTDOWN, гейты canRunHud/Render +├── events/ — EventBus (@EventHandler, приоритеты, гейт по Module) +│ └── impl/ — конкретные события (Tick, RenderHud, Packet, Lightmap...) +├── config/ — MainConfig, SettingDef, сериализация (.lvcfg, legacy .cbcfg) +│ ├── values/ — ConfigValue: bool/num/mode/color/set/... (есть тесты) +│ └── profile/ — профили конфигов (collect/apply/diff/binary codec) +├── features/ +│ ├── module/ — Module, ModuleCategory, ModuleInfo, ModuleManager, +│ │ └── modules/... — сами модули по категориям +│ ├── gui/ — ClickGUI, HUD (draggable + nondraggable), BetterChat +│ ├── command/ — чат-команды (@CommandInfo, ClassGraph-поиск) +│ ├── relations/ — друзья/враги/стаф (PlayerRelations, CategoryService) +│ └── security/ — AntiBot-фильтр, StaffTracker +├── render/engine/ — Renderer2D/3D, пайплайны, шрифты, MeshBuilder +├── mixins/ — @Mixin-классы (см. правило выше) +└── util/ — чистые утилиты (text/animation/time/...) — тут живут тесты +``` + +--- + +## 4. Как добавить МОДУЛЬ + +Модули находятся в `features/module/modules/<категория>/`. Категории: +`COMBAT · MOVEMENT · PLAYER · VISUALS · MISC` (см. `ModuleCategory`). +Легит-направление: читерские модули (автоудары, полёты, speed...) **удаляем, не добавляем**. + +Регистрация автоматическая: **ClassGraph сканирует `@ModuleInfo`** — ничего никуда +дописывать не надо. Пустой конструктор обязателен. + +Шаблон: + +```java +@ModuleInfo(id = "mymodule", displayName = "My Module", category = ModuleCategory.VISUALS) +public class MyModule extends Module { + + private static final String SETTING_X = "x_amount"; + private final NumberValue amount = num("myModuleAmount", SETTING_X, 5, 1, 10); + + @Override + public void onTick() { + Minecraft mc = Minecraft.getInstance(); // локально, не в поле! + if (mc.level == null) return; + // ... + } + + @EventHandler + private void onRenderHud(RenderHudEvent event) { /* авто-подписка через EventBus */ } +} +``` + +Ключевые хуки `Module`: `onEnable/onDisable/onTick/onFrame`, +`onRenderHudEngine(...)`, `onRenderWorldEngine(...)`, `getWorldPhase()/getHudPhase()`. +Хелперы настроек сами объявляют `SettingDef`: `bool/num/mode/color/colorNoAlpha/ +enumSetting/text/group/bind` + `visibleWhen(value, () -> cond)`. +Модуль-лист по умолчанию недоступен цели — смотри также `boolCommon`/`modeCommon` +для общих схем (`CommonSettingSchemas`). + +Доступ к чужому модулю: `Modules.get(X.class)`, проверка состояния — `isEnabled()`. + +Мировой рендер (боксы/линии) — через `Renderer3D.batch(WORLD_COLORED_LINES, DepthMode...)` +и MeshBuilder; готовые куски геометрии — `EspMeshBuilder.addOutlineBox` и т.п. + +--- + +## 5. Как добавить HUD-элемент + +Два вида, оба ищутся ClassGraph-ом автоматически: + +**A. Draggable (перетаскиваемый, вкладка Settings → HUD):** +`features/gui/hud/draggable/impl/`, аннотация `@HudElementInfo(id, displayName, +enabledByDefault, order)`, базовый класс `DraggableHudElement`. +Обязательно: `applyDefaultPosition(screenW, screenH)`, рендер в `renderEngine(...)`, +`usesEngineRenderer() = true`. Позиция сохраняется через `layoutValue`. + +**B. Static (фиксированный, вкладка Settings → UI):** +`features/gui/hud/nondraggable/impl/`, аннотация `@HudElementRegister(order=N)`, +базовый класс `AbstractHudElement`, конструктор `super(id, "Title", defaultEnabled)`, +синглтон `public static final X INSTANCE` (лоадер забирает его из статического поля). +Чтобы карточка появилась во вкладке **UI**, добавь `new StaticHudCard("", "")` +в `STATIC_UI_CARDS` в `MenuSettingsResolver.java` (draggable-виджеты там не нужны — +они собираются из реестра сами). + +Цвет из темы: `Theme.theme().accent()/windowBg()/textMuted()`; альфа — +`HudRenderUtil.setAlpha(argb, a)`; текст — `Fonts.renderer(name, type, fallback)` +(`begin(scale,...)` → `getWidth`/`render` → `end()`). + +--- + +## 6. Как добавить СОБЫТИЕ / МИКСИН + +1. Событие: наследуй `Event` (есть `cancel()`), положи в `events/impl/`. +2. Публикация: `Events.BUS.post(new MyEvent(...))` из миксина. +3. Миксин: класс с `@Mixin(Target.class)` в `mixins/` **и имя в `lovisual.mixins.json`** + (секции `mixins`/`client`). Миксин, которого нет в json, не применится. +4. Если миксин опциональный (зависит от чужого мода, напр. Sodium) — цель может + отсутствовать: Mixin напечатает WARN и пройдёт (см. `SodiumChunkMeshFormatsMixin`). + Такой WARN — норма, краш — нет. +5. Чистая логика из миксина — вобы в обычный класс вне `mixins/`-пакета (см. правило про `HandDynamics`). + +--- + +## 7. Настройки, конфиг и i18n + +- Конфиги модулей/HUD пишутся в `<game>/lovisual/` в формате **`.lvcfg`** + (legacy `.cbcfg` читается). Профили — `config/profile/` (кнопка в ClickGUI → Configs). +- Каждой настройке нужен i18n-ключ в **обоих** файлах + `src/main/resources/assets/lovisual/lang/{en_us,ru_ru}.json`: + - модуль: `module.<id>.description`, настройки `setting.<id>.<setting_id>`, + опции режима `setting.<id>.<setting_id>.option.<Option>`; + - draggable HUD: `hud.draggable.<id>` / `setting.draggable.<id>.<key>`; + - static HUD: `setting.hud_element.<id>.<key>` (см. Watermark, Swap Tooltip). +- `setting_id` — snake_case (`"show_ping"`); configName —camelCase с префиксом мода + (`"watermark_show_ping"`) — из него авто-выводится id. +- Пропущенный ключ ловит `MissingI18nReporter` в дев-окружении — проверяй консоль. + +--- + +## 8. Тесты + +`src/test/java/` зеркалит main. Пишутся **только на чистую логику** (без Minecraft- +рантайма): парсеры, математика, codec'ы, матчинги. Примеры: +- `util/text/NameProtectorTest`, `LegacyTextUtilTest` — парсинг/подмена, +- `config/values/*Test` — значения конфига, +- `config/profile/ConfigProfileValueCodecTest` — бинарный round-trip, +- `events/EventBusTest` — события (нужен `Lifecycle.activate()` в `@BeforeAll`). + +Если фича завязана на Minecraft — выделяй статический pure-метод +(`static int tint(...)` / `static record Layout(...)`) и тесть его (см. `Light.tintAmbient`). + +--- + +## 9. Чеклист перед коммитом + +- [ ] `./gradlew build` зелёный, `./gradlew test` зелёный +- [ ] новых файлов >200 строк нет; папка не разрослась >4 файлов +- [ ] нет полей `Minecraft` / `getInstance()` в полях +- [ ] lang-ключи en_us + ru_ru добавлены для всех новых настроек +- [ ] тест на новую чистую логику есть +- [ ] в mixin-пакете только `@Mixin`-классы +- [ ] TODO.md обновлён (галка/заметка), если это этап плана +- [ ] в чужой код (`ref/`) не заглядывался копипаст diff --git a/combatant-client-26.2/DEV_GUIDE.md b/combatant-client-26.2/DEV_GUIDE.md index 8861c9e..7df9f02 100644 --- a/combatant-client-26.2/DEV_GUIDE.md +++ b/combatant-client-26.2/DEV_GUIDE.md @@ -474,11 +474,16 @@ BetterTooltips, BetterButtons, DynamicIsland). Наследуют `BaseHudElemen Интерфейс `ClientCommand` + аннотация `@CommandInfo`, регистрация — вручную в `CommandManager`. +Префикс команд в чате — **`%`** (старый `@` тоже принимается). Источник истины — +`CommandPrefix` (`CHAR`/`STRING`/`isCommandLike`/`strip`). Автокомплит: имена команд + +аргументы из `suggest()`, а если команда не дала вариантов — подсказка-usage через +`UsageHints` (например `%toggle <module>`). + ```java @CommandInfo( id = "toggle", aliases = "t", - usage = "@toggle <module> [on|off|toggle]", + usage = "%toggle <module> [on|off|toggle]", descriptionKey = "command.toggle.description" ) public final class ToggleCommand implements ClientCommand { diff --git a/combatant-client-26.2/README.md b/combatant-client-26.2/README.md index 6d56879..08649e4 100644 --- a/combatant-client-26.2/README.md +++ b/combatant-client-26.2/README.md @@ -19,7 +19,7 @@ Developed and maintained by loki5512344. ### Overview -LoVisual is an open-source utility and PvP client for Minecraft. It provides configurable combat, movement, visual, automation, HUD, and UI features with a custom ClickGUI, theme system, and addon API. +LoVisual is an open-source legit utility client for Minecraft. It provides configurable visual, HUD, and UI features with a custom ClickGUI, theme system, and addon API. LoVisual is developed and maintained by [loki5512344](https://github.com/loki5512344). The goal is to keep adding new features and maintain compatibility with the latest Minecraft versions. @@ -34,14 +34,13 @@ LoVisual is developed and maintained by [loki5512344](https://github.com/loki551 | Feature | Description | |---------|-------------| -| Combat | KillAura, AutoCrystal, AutoAnchor, AutoBed, AutoBow, Criticals, Reach, Velocity, Backtrack, and more | -| Movement | Speed, Flight, ElytraFly, ElytraHelper, Sprint, Timer, Strafe, TargetStrafe, Scaffold, Parkour | -| Visuals | ESP, NameTags, Chams, Tracers, Crosshair, Zoom, Freecam, FullBright, MotionBlur, NoRender | -| Player | AutoTool, AutoEat, AutoArmor, AutoTotem, Scaffold, ClickPearl, FakeLag, XCarry | -| Misc | BetterMinecraft, FakePlayer, HitSounds, Panic, NoSound | -| HUD | Draggable elements, TargetHUD, ModuleList, Watermark, Armor, Potions, Keystrokes, Coords, Notifications, Scoreboard | +| Combat | ProjectilePuncher, Reach, RubberHand, SpearAssist, TPSSync, PvpCooldowns, NoFriendDamage, TargetESP, Predictions, DamageTint | +| Visuals | ESP, NameTags, Chams, Crosshair, FullBright, MotionBlur, NoRender, Freecam, Hitboxes, Light, ChinaHat, Trinkets, and more | +| Player | AutoTool, ClickPearl, FakeLag, NoDelay, LockSlot, AutoRespawn, DeathCoords, NoInteract | +| Misc | BetterMinecraft, FakePlayer, HitSounds, NameProtect, MessageFilter, NoSound, DefineTarget | +| HUD | Draggable & static elements: TargetHUD, ModuleList, Watermark, Reach, Armor, Potions, Keystrokes, Coords, Notifications, Scoreboard | | ClickGUI | Full-featured GUI with module browser, settings editor, theme editor, config profiles | -| Config profiles | `.lvisual` profiles for modules, HUD, and themes — shareable between users | +| Config profiles | `.lvcfg` profiles for modules, HUD, and themes — shareable between users | | Addon API | Versioned addon API for extending the client | | Custom renderer | Custom UI rendering with MSDF fonts, SVG icons, and shader support | | Iris/Sodium/Xaero | Optional compatibility with popular mods | @@ -75,7 +74,7 @@ LoVisual is developed and maintained by [loki5512344](https://github.com/loki551 ### Обзор -LoVisual — открытый утилитарный и PvP-клиент для Minecraft. Включает настраиваемые боевые, двигательные, визуальные и автоматизированные модули, кастомный ClickGUI, систему тем и API для аддонов. +LoVisual — открытый легитный утилитарный клиент для Minecraft. Включает настраиваемые визуальные, HUD и UI-модули, кастомный ClickGUI, систему тем и API для аддонов. LoVisual разрабатывается и поддерживается [loki5512344](https://github.com/loki5512344). Цель — добавлять новые возможности и поддерживать актуальные версии Minecraft. @@ -90,14 +89,13 @@ LoVisual разрабатывается и поддерживается [loki551 | Возможность | Описание | |-------------|----------| -| Combat | KillAura, AutoCrystal, AutoAnchor, AutoBed, AutoBow, Criticals, Reach, Velocity, Backtrack и другие | -| Movement | Speed, Flight, ElytraFly, ElytraHelper, Sprint, Timer, Strafe, TargetStrafe, Scaffold, Parkour | -| Visuals | ESP, NameTags, Chams, Tracers, Crosshair, Zoom, Freecam, FullBright, MotionBlur, NoRender | -| Player | AutoTool, AutoEat, AutoArmor, AutoTotem, Scaffold, ClickPearl, FakeLag, XCarry | -| Misc | BetterMinecraft, FakePlayer, HitSounds, Panic, NoSound | -| HUD | Перетаскиваемые элементы: TargetHUD, ModuleList, Watermark, Armor, Potions, Keystrokes, Coords, Notifications, Scoreboard | +| Combat | ProjectilePuncher, Reach, RubberHand, SpearAssist, TPSSync, PvpCooldowns, NoFriendDamage, TargetESP, Predictions, DamageTint | +| Visuals | ESP, NameTags, Chams, Crosshair, FullBright, MotionBlur, NoRender, Freecam, Hitboxes, Light, ChinaHat, Trinkets и другие | +| Player | AutoTool, ClickPearl, FakeLag, NoDelay, LockSlot, AutoRespawn, DeathCoords, NoInteract | +| Misc | BetterMinecraft, FakePlayer, HitSounds, NameProtect, MessageFilter, NoSound, DefineTarget | +| HUD | Перетаскиваемые и статические элементы: TargetHUD, ModuleList, Watermark, Reach, Armor, Potions, Keystrokes, Coords, Notifications, Scoreboard | | ClickGUI | Полноценный GUI с браузером модулей, редактором настроек, тем и профилей конфигов | -| Профили `.lvisual` | Профили для модулей, HUD и тем — можно шарить между пользователями | +| Профили `.lvcfg` | Профили для модулей, HUD и тем — можно шарить между пользователями | | Addon API | Версионированное API для аддонов | | Кастомный рендер | UI-рендеринг с MSDF-шрифтами, SVG-иконками и шейдерами | | Iris/Sodium/Xaero | Опциональная совместимость с популярными модами | @@ -137,7 +135,7 @@ LoVisual разрабатывается и поддерживается [loki551 ### Docs -- [Architecture](docs/ARCHITECTURE.md) +- [Development guide](DEVELOPMENT.md) — project rules, how to add modules/HUD/events/config ### License diff --git a/combatant-client-26.2/TODO.md b/combatant-client-26.2/TODO.md index e39d78a..33e7190 100644 --- a/combatant-client-26.2/TODO.md +++ b/combatant-client-26.2/TODO.md @@ -148,8 +148,10 @@ чистый import-churn без функциональной ценности; делать только отдельным скриптовым проходом ## Фаза 6: Тесты (пункт 6) -- [ ] Юнит-тесты: EventBus, конфиг, утилиты (игровые классы — mock) -- [ ] По мере переписи модулей — тест на каждый +- [x] Юнит-тесты: EventBus (12), конфиг (values: NumberValue/Boolean/String/Set/ItemIdSet/BooleanMap, + ModeValue, EnumValue, RGB/RGBA color; profile: DiffEngine, ValueCodec), + утилиты (LegacyTextUtil — 12). Игровые классы без моков: тесты только на чистую логику. +- [x] По мере переписи модулей — тест на каждый (Текущий итог: 292 теста, все зелёные) ## Фаза 7: QoL-модули из Delta (легит) — ПЛАН, обсуждаем > Референс: `ref/delta-26.2-main` (чит-клиент, код обфусцирован — берём только идеи, @@ -167,41 +169,64 @@ - [x] Правило папок: максимум 4 файла (было 3) ### Перенос (список пользователя) -- [ ] **StreamerMode** (misc, ~80 у них) — при записи/стриме скрывает: ники игроков, - имена друзей, названия серверов («Анархия-N») в чате/таб-листе/скорборде; скрытие скинов. - Интеграция: наши BetterChat + CustomTabList + Scoreboard-виджет + рендер скинов. +- [x] **StreamerMode** (misc) — маскирует ники-подобные токены и названия серверов в чате + (общая точка NameProtect: ChatComponentMixin.addMessage), скины заменяются на + стандартные (AbstractClientPlayerMixin.getSkin). Чистая логика — `util/text/NameMasker` + (тест NameMaskerTest). - [x] **LockSlot** (player, ~36) — запрет выброса из выбранных слотов 1–9 (анти-случайный Q), опция «только в PVP», предупреждение в чат. Точка: перехват drop-пакета. - [x] **AutoRespawn** (player, ~28) — авто-возрождение после смерти (tick на death screen). -- [ ] **AutoLeave** (player, ~61) — выход в хаб по триггерам: малое ХП, игроки рядом (N, радиус), задержка. -- [ ] **AutoAccept** (player, ~67) — авто-принятие запросов: клан/ТП; опция «только от друзей» - через наш PlayerRelations. Точка: перехват chat-пакетов. -- [ ] **SoundReducer** (player, ~42) — приглушение выбранных звуков/категорий (громкость 0..N), - в отличие от нашего NoSound (он только вкл/выкл всего). Вопрос: новый модуль или развитие NoSound. -- [ ] **Wasted** (misc, ~100) — ОТМЕНЕНО (решение пользователя) -- [ ] **ThirdPerson** (player, ~77) — свободный обзор от 3-го лица без поворота движения - (по нажатию/зажатию бинда). Не дублирует Freecam (он про отдельную камеру-сущность). - Точка: наш CameraMixin. -- [ ] **UseTracker** (player, ~86) — уведомления об использованиях: тотем, зелья, предметы. - Вывод через наш HudNotifier. Точка: пакеты + инвентарь. -- [x] **NoServerPack** (misc) — у Delta ПУСТЫШКА (пустой класс). Сделаем по-настоящему: - авто-отклонение принудительного серверного ресурспака (спрашивать/отклонять всегда). - Точка: наш ClientCommonPacketListenerImplMixin. +- [x] **AutoLeave** (player) — выход по триггерам: низкое ХП, N игроков в радиусе + (друзья игнорируются опцией), задержка + предупреждение. Выход через + `mc.disconnect(null, false)`; чистая state-machine `LeavePolicy` (AutoLeaveTest). +- [x] **AutoAccept** (player) — авто-ответ на ТП/клан-запросы: паттерны фраз (SetValue), + команда ответа, «только друзья» (PlayerRelations), задержка, кулдаун ≥500мс. + Пакетный путь: PvpChatEvent (handleSystemChat/handlePlayerChat), отправка + `connection.sendCommand(...)`. Чистый парсер `TeleportRequestParser` (AutoAcceptTest). +- [x] **SoundReducer** (player, ~42) — РЕШЕНО: развитие NoSound, а не новый модуль. + Режимы Mute (старое поведение через ClientPacketListenerMixin) и Reduce — + новая громкость 1–100% через SoundEngineMixin (@Redirect getVolume в + SoundEngine.play). Чистая математика `NoSound.resolveScale` (NoSoundTest). +- [x] **Wasted** (misc, ~100) — ОТМЕНЕНО (решение пользователя) +- [x] **ThirdPerson** (player) — свободный обзор от 3-го лица по удержанию бинда (L, + настраивается), движение игрока не поворачивается. MouseMixin.turnPlayer копит + orbit-yaw/pitch, CameraMixin.update (TAIL) ставит камеру за игрока по орбите + (+ cull-frustum). Орбиты сбрасываются, когда бинд отпущен. +- [x] **UseTracker** (player) — уведомления: мои тотемы (N осталось), тотемы других, + эффекты оппонентов (группа: invisibility/speed/strength/regeneration/fire_res/weakness). + Пакетный путь: ClientboundEntityEventPacket (event 35) + ClientboundUpdateMobEffectPacket. + Тест UseTrackerTest. +- [x] **NoServerPack** (misc) — авто-отклонение принудительного серверного ресурспака + (спрашивать/отклонять всегда). Точка: ClientCommonPacketListenerImplMixin. - [x] **NoFriendDamage** (combat, ~19) — отмена атаки, если цель — друг (PlayerRelations). - Точка: перехват атаки (AttackEvent у них; у нас миксин на attack/interact-пакет). -- [x] **TapeMouse** (combat, ~66) — автоклик по таймеру. ⚠️ ПРОТИВОРЕЧИТ легит-политике Фазы 3.5 - (тот же класс, что удалённые AutoAttack/TriggerBot). По умолчанию НЕ делаем / делаем - легит-вариант «напоминание о клике» без автоматического клика — РЕШИТЬ. +- [x] **TapeMouse** — ОСТАВЛЕН (решение владельца 2026-09-08): автоклик по таймеру + (Attack/Use) для AFK-ферм, интервал, опция «только при зажатии». Считаем + допустимым исключением из Фазы 3.5 по явному решению владельца. ### Кандидаты из отчёта (не в списке пользователя — обсудить) - [x] **DeathCoords** (~27) — координаты последней смерти в чат. Дёшево, полезно. -- [ ] **ItemScroller** — перекладывание предметов прокруткой в инвентарях. Крупный QoL (~200+). -- [ ] **ColorPlus** — ⚠️ это по сути наш удалённый PostFX (экспозиция/гамма/контраст/насыщенность/ - виньетка/резкость/LUT). Восстанавливать под новым именем или забыть — РЕШИТЬ. -- [ ] **Arrows / off-screen индикаторы** — дополнение к Pointers (у нас их нет вообще). +- [x] **ItemScroller** (misc) — shift-перекладка предметов колёсиком в контейнерах: + QUICK_MOVE клик по слоту под курсором, иначе слот хотбара (опция). + Точка: AbstractContainerScreenMixin.mouseScrolled; чистая логика + `ScrollMovePolicy` (ScrollMovePolicyTest). +- [x] **Arrows / off-screen индикаторы** — ОТМЕНЕНО (решение владельца 2026-09-08). ### Проверка после каждого модуля: `./gradlew build` ✓, тесты на чистую логику ✓ +## Фаза 7.5: Команды — префикс % + автокомплит +- [x] Префикс команд сменён `@` → **`%`** (старый `@` принимается как legacy). + Источник истины — `features/command/CommandPrefix` (CHAR/STRING/isCommandLike/strip); + вызовы обновлены: CommandManager.handle/suggest, ScreenMixin (RunCommand), + BetterChat (TabHandler, SuggestionState, SuggestionRenderer, ContextMenuHandler), + usage-строки и RunCommand-ссылки всех команд (help/modules и др.). +- [x] Аргументный автокомплит дополнен usage-подсказками: если `suggest()` команды + не дал вариантов на позиции — показывается подсказка вида `%toggle <module>` + (`features/command/UsageHints`, парсит `<...>`/`[...]` из usage-строки). + Существующий UI автокомплита BetterChat (стрелки/Tab/клик) работает как раньше. + +### Проверка: `./gradlew build` ✓, `./gradlew test` ✓ (CommandPrefixTest, UsageHintsTest, +### CommandDiscoveryTest обновлён) + ## Фаза 8: Структура «как у Delta» (маленькие файлы) — ПЛАН > Правило ≤200 уже есть (Фаза 1–3), но долг: ~240 файлов >200 строк. > У Delta файлы компактные и слои разделены: api / client(features, ui, utils, processors) / diff --git a/combatant-client-26.2/config/lovisual/hud/draggable/watermark.json b/combatant-client-26.2/config/lovisual/hud/draggable/watermark.json new file mode 100644 index 0000000..3eaf4d9 --- /dev/null +++ b/combatant-client-26.2/config/lovisual/hud/draggable/watermark.json @@ -0,0 +1,13 @@ +{ + "enabled": true, + "watermark_scale": 1.0, + "watermark_color_mode": "Theme", + "watermark_show_fps": true, + "watermark_show_ping": true, + "watermark_show_server": true, + "watermark_show_user": false, + "watermark_show_dimensions": false, + "watermark_title_color": "#5CC8E7", + "watermark_meta_color": "#9B9B9B", + "watermark_bg_color": "#1A1D23" +} diff --git a/combatant-client-26.2/config/lovisual/hud/nondraggable/hud_element_reach_display.json b/combatant-client-26.2/config/lovisual/hud/nondraggable/hud_element_reach_display.json new file mode 100644 index 0000000..6841ff9 --- /dev/null +++ b/combatant-client-26.2/config/lovisual/hud/nondraggable/hud_element_reach_display.json @@ -0,0 +1,10 @@ +{ + "enabled": true, + "reach_scale": 1.0, + "reach_keep_ms": 2500.0, + "reach_sync_theme": true, + "reach_show_max": true, + "reach_highlight_overreach": true, + "reach_value_color": "#5CC8E7", + "reach_bg_color": "#1A1D23" +} diff --git a/combatant-client-26.2/config/lovisual/hud/nondraggable/hud_element_watermark.json b/combatant-client-26.2/config/lovisual/hud/nondraggable/hud_element_watermark.json new file mode 100644 index 0000000..d8562db --- /dev/null +++ b/combatant-client-26.2/config/lovisual/hud/nondraggable/hud_element_watermark.json @@ -0,0 +1,12 @@ +{ + "enabled": true, + "watermark_scale": 1.0, + "watermark_sync_theme": true, + "watermark_show_version": true, + "watermark_show_user": false, + "watermark_show_fps": true, + "watermark_show_ping": true, + "watermark_title_color": "#5CC8E7", + "watermark_meta_color": "#9B9B9B", + "watermark_bg_color": "#1A1D23" +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/CommandManager.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/CommandManager.java index 4674d29..fa68780 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/CommandManager.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/CommandManager.java @@ -55,10 +55,10 @@ public enum CommandManager { public static boolean handle(String raw) { if (raw == null) return false; String trimmed = raw.trim(); - if (!isCommandLike(trimmed)) return false; + if (!CommandPrefix.isCommandLike(trimmed)) return false; if (!initialized) init(); - String body = trimmed.substring(1).trim(); + String body = CommandPrefix.strip(trimmed); if (body.isEmpty()) return false; String[] parts = body.split("\\s+"); if (parts.length == 0) return false; @@ -66,7 +66,8 @@ public enum CommandManager { ClientCommand cmd = find(name); if (cmd == null) { - CommandOutput.error("Unknown client command: @" + name + ". Use @help."); + CommandOutput.error("Unknown client command: " + CommandPrefix.STRING + name + + ". Use " + CommandPrefix.STRING + "help."); return true; } if (!cmd.isAvailable()) { @@ -82,7 +83,9 @@ public enum CommandManager { public static List<Suggestion> suggest(String input, int cursor) { if (input == null) return List.of(); int start = firstNonWhitespace(input); - if (start < 0 || input.charAt(start) != '@') return List.of(); + if (start < 0) return List.of(); + char prefix = input.charAt(start); + if (prefix != CommandPrefix.CHAR && prefix != CommandPrefix.LEGACY) return List.of(); if (!initialized) init(); int cursorClamped = Math.max(start + 1, Math.min(cursor, input.length())); @@ -110,6 +113,12 @@ public enum CommandManager { args.addAll(Arrays.asList(parts).subList(1, parts.length)); CommandContext ctx = new CommandContext(Minecraft.getInstance(), input, cmdName, args); suggestions = cmd.suggest(ctx, argIndex, token); + if (suggestions == null || suggestions.isEmpty()) { + // Fall back to the usage hint so there is always guidance on + // what can be typed at the current position. + String hint = UsageHints.forCommand(cmd, argIndex); + suggestions = hint == null ? List.of() : List.of(hint); + } } if (suggestions == null || suggestions.isEmpty()) return List.of(); @@ -138,15 +147,6 @@ public enum CommandManager { return null; } - private static boolean isCommandLike(String text) { - for (int i = 0; i < text.length(); i++) { - char c = text.charAt(i); - if (Character.isWhitespace(c)) continue; - return c == '@'; - } - return false; - } - private static List<String> suggestCommandNames(String token) { String lower = token == null ? "" : token.toLowerCase(); List<String> out = new ArrayList<>(); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/CommandMetadata.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/CommandMetadata.java index 7ce58e3..36e0f6a 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/CommandMetadata.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/CommandMetadata.java @@ -36,7 +36,7 @@ public final class CommandMetadata { boolean localizedDescription) { this.id = normalizeId(id); this.aliases = normalizeAliases(aliases, this.id); - this.usage = usage == null || usage.isBlank() ? "@" + this.id : usage.trim(); + this.usage = normalizeUsage(usage, this.id); if (description == null || description.isBlank()) { this.description = this.usage; this.localizedDescription = false; @@ -94,15 +94,34 @@ public final class CommandMetadata { private static CommandMetadata fromLegacy(ClientCommand command) { String id = command.name(); List<String> aliases = command.aliases(); + // Legacy default usage()/description() resolve back to the id with no + // prefix; they are normalized here the same way as annotated strings. String usage = command.usage(); String description = command.description(); - return new CommandMetadata(id, aliases, usage, description, false); + String normalizedUsage = usage.equals(id) || usage.isBlank() ? null : usage; + String normalizedDescription = + description.equals(id) || description.equals(usage) || description.isBlank() ? null : description; + return new CommandMetadata(id, aliases, normalizedUsage, normalizedDescription, false); + } + + private static String normalizeUsage(String usage, String id) { + if (usage == null || usage.isBlank()) return CommandPrefix.STRING + id; + String trimmed = usage.trim(); + // Usage strings were written with the legacy '@' prefix; re-prefix them + // with the active prefix so help output stays consistent. + if (!trimmed.isEmpty() && (trimmed.charAt(0) == CommandPrefix.LEGACY + || trimmed.charAt(0) == CommandPrefix.CHAR)) { + trimmed = trimmed.substring(1); + } + return CommandPrefix.STRING + trimmed; } private static String normalizeId(String id) { if (id == null || id.isBlank()) throw new IllegalArgumentException("Command id cannot be blank"); String normalized = id.trim().toLowerCase(Locale.ROOT); - if (normalized.charAt(0) == '@') normalized = normalized.substring(1); + if (normalized.charAt(0) == '@' || normalized.charAt(0) == CommandPrefix.CHAR) { + normalized = normalized.substring(1); + } if (normalized.isBlank() || normalized.chars().anyMatch(Character::isWhitespace)) { throw new IllegalArgumentException("Invalid command id: " + id); } @@ -115,7 +134,9 @@ public final class CommandMetadata { for (String alias : aliases) { if (alias == null || alias.isBlank()) continue; String value = alias.trim().toLowerCase(Locale.ROOT); - if (value.charAt(0) == '@') value = value.substring(1); + if (!value.isEmpty() && (value.charAt(0) == '@' || value.charAt(0) == CommandPrefix.CHAR)) { + value = value.substring(1); + } if (value.isBlank() || value.chars().anyMatch(Character::isWhitespace) || value.equals(id)) continue; if (!normalized.contains(value)) normalized.add(value); } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/CommandPrefix.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/CommandPrefix.java new file mode 100644 index 0000000..f8b0016 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/CommandPrefix.java @@ -0,0 +1,53 @@ +/* + * 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.command; + +/** + * Single source of truth for the client command prefix. Messages typed in chat + * starting with this character are handled by the client and never sent to the + * server. Also covers the legacy '@' so old profiles/links keep working. + */ +public final class CommandPrefix { + /** The active client command prefix. */ + public static final char CHAR = '%'; + /** Legacy prefix, still accepted for compatibility. */ + public static final char LEGACY = '@'; + /** The prefix as a string (for messages, usage strings and click events). */ + public static final String STRING = String.valueOf(CHAR); + + private CommandPrefix() { + } + + /** @return true when the first non-space character is the (or a legacy) prefix */ + public static boolean isCommandLike(String text) { + return leadingPrefix(text) != 0; + } + + /** + * @return the prefix char found before any command text, 0 when the input + * does not start with a command prefix + */ + public static char leadingPrefix(String text) { + if (text == null) return 0; + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (Character.isWhitespace(c)) continue; + return (c == CHAR || c == LEGACY) ? c : 0; + } + return 0; + } + + /** Strips the leading prefix and surrounding whitespace; null-safe. */ + public static String strip(String text) { + if (text == null) return null; + String trimmed = text.trim(); + if (trimmed.isEmpty()) return trimmed; + char p = trimmed.charAt(0); + return (p == CHAR || p == LEGACY) ? trimmed.substring(1).trim() : trimmed; + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/UsageHints.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/UsageHints.java new file mode 100644 index 0000000..6b9ef62 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/UsageHints.java @@ -0,0 +1,83 @@ +/* + * 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.command; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** + * Builds usage-hint suggestions shown in the autocomplete popup when a command + * has no concrete suggestions for the argument position the user is typing at. + * Pure string logic, unit-testable without Minecraft. + */ +public final class UsageHints { + private UsageHints() { + } + + /** + * Extracts the placeholder at {@code argIndex} (1-based, after the command + * name) from a usage string like "%toggle <module> [on|off|toggle]". + * + * @return hint without brackets, e.g. "module" or "on|off|toggle"; + * null when the usage has nothing at that position + */ + public static String placeholder(String usage, int argIndex) { + if (usage == null || usage.isBlank() || argIndex < 1) return null; + return placeholders(usage).stream() + .skip(argIndex - 1L) + .findFirst() + .orElse(null); + } + + /** + * Usage hint with the command prefix: "<module>" becomes "%toggle <module>" + * so the popup always shows what the full input looks like so far. + */ + public static String forCommand(ClientCommand command, int argIndex) { + if (command == null || argIndex < 1) return null; + String usage = command.metadata().usage(); + String placeholder = placeholder(usage, argIndex); + if (placeholder == null) return null; + + String head = commandHead(usage); + String prefix = CommandPrefix.STRING + (head.isEmpty() ? command.metadata().id() : head); + return prefix + " <" + placeholder + ">"; + } + + /** Splits "[list|add|remove] [player]" into ["list|add|remove", "player"]. */ + static List<String> placeholders(String usage) { + List<String> out = new ArrayList<>(); + StringBuilder current = new StringBuilder(); + boolean inside = false; + for (int i = 0; i < usage.length(); i++) { + char c = usage.charAt(i); + if (c == '[' || c == '<') { + inside = true; + current.setLength(0); + } else if (c == ']' || c == '>') { + inside = false; + if (!current.isEmpty()) out.add(current.toString()); + current.setLength(0); + } else if (inside) { + current.append(c); + } + } + if (inside && !current.isEmpty()) out.add(current.toString()); + return List.copyOf(out); + } + + /** First word of the usage body, e.g. "toggle" from "%toggle <module>". */ + static String commandHead(String usage) { + String trimmed = usage.trim(); + if (trimmed.startsWith(CommandPrefix.STRING)) trimmed = trimmed.substring(1); + int space = trimmed.indexOf(' '); + String head = space < 0 ? trimmed : trimmed.substring(0, space); + return head.toLowerCase(Locale.ROOT); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/AddonsCommand.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/AddonsCommand.java index 45f9836..d5023a1 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/AddonsCommand.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/AddonsCommand.java @@ -20,7 +20,7 @@ import java.util.Locale; @CommandInfo( id = "addons", - usage = "@addons [list|scan|enable|disable] [id]", + usage = "%addons [list|scan|enable|disable] [id]", descriptionKey = "command.addons.description" ) public final class AddonsCommand implements ClientCommand { diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/BindCommand.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/BindCommand.java index cfcc782..dd6cd35 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/BindCommand.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/BindCommand.java @@ -20,7 +20,7 @@ import java.util.Locale; @CommandInfo( id = "bind", aliases = {"binds", "keybind"}, - usage = "@bind <module> <key|combo|none> | @bind list", + usage = "%bind <module> <key|combo|none> | @bind list", descriptionKey = "command.bind.description" ) public final class BindCommand implements ClientCommand { diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/ConfigCommand.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/ConfigCommand.java index c4569e2..fc4f6fc 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/ConfigCommand.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/ConfigCommand.java @@ -21,7 +21,7 @@ import java.util.Locale; @CommandInfo( id = "config", aliases = {"cfg", "settings"}, - usage = "@config [save|load|path]", + usage = "%config [save|load|path]", descriptionKey = "command.config.description" ) public final class ConfigCommand implements ClientCommand { diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/CoordinatesCommand.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/CoordinatesCommand.java index ce61eda..66458e8 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/CoordinatesCommand.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/CoordinatesCommand.java @@ -17,7 +17,7 @@ import java.util.Locale; @CommandInfo( id = "coordinates", aliases = {"coords", "position", "pos"}, - usage = "@coordinates", + usage = "%coordinates", descriptionKey = "command.coordinates.description" ) public final class CoordinatesCommand implements ClientCommand { diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/HelpCommand.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/HelpCommand.java index 31cad51..5bc9ff2 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/HelpCommand.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/HelpCommand.java @@ -10,6 +10,7 @@ package dev.loki.lovisual.features.command.impl; import dev.loki.lovisual.features.command.ClientCommand; import dev.loki.lovisual.features.command.CommandContext; import dev.loki.lovisual.features.command.CommandInfo; +import dev.loki.lovisual.features.command.CommandPrefix; import dev.loki.lovisual.features.command.CommandManager; import dev.loki.lovisual.features.command.CommandMetadata; import dev.loki.lovisual.features.command.CommandOutput; @@ -25,7 +26,7 @@ import java.util.Locale; @CommandInfo( id = "help", aliases = {"commands", "cmds"}, - usage = "@help [page|command]", + usage = "%help [page|command]", descriptionKey = "command.help.description" ) public final class HelpCommand implements ClientCommand { @@ -101,13 +102,13 @@ public final class HelpCommand implements ClientCommand { private static MutableComponent commandLink(ClientCommand command) { CommandMetadata metadata = command.metadata(); - String insert = "@" + metadata.id() + " "; + String insert = CommandPrefix.STRING + metadata.id() + " "; String aliases = metadata.aliases().isEmpty() ? "none" : String.join(", ", metadata.aliases()); Component hover = Component.literal(metadata.description() + "\nUsage: " + metadata.usage() + "\nAliases: " + aliases + "\nClick to insert"); - return Component.literal("@" + metadata.id()) + return Component.literal(CommandPrefix.STRING + metadata.id()) .withStyle(style -> style .withColor(COMMAND_COLOR) .withUnderlined(true) @@ -120,13 +121,13 @@ public final class HelpCommand implements ClientCommand { .withStyle(style -> style .withBold(true) .withColor(ACTIVE_COLOR) - .withClickEvent(new ClickEvent.RunCommand("@help " + page)) + .withClickEvent(new ClickEvent.RunCommand("%help " + page)) .withHoverEvent(new HoverEvent.ShowText(Component.literal("Open help page " + page)))); } private static void showDetails(ClientCommand command) { CommandMetadata metadata = command.metadata(); - MutableComponent message = Component.literal("@" + metadata.id()) + MutableComponent message = Component.literal(CommandPrefix.STRING + metadata.id()) .withStyle(style -> style.withBold(true).withColor(TITLE_COLOR)); message.append(Component.literal("\n" + metadata.description()).withStyle(style -> style.withColor(COMMAND_COLOR))); message.append(Component.literal("\nUsage: " + metadata.usage()).withStyle(style -> style.withColor(MUTED_COLOR))); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/HideCommand.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/HideCommand.java index ae78993..ac28ff2 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/HideCommand.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/HideCommand.java @@ -19,7 +19,7 @@ import java.util.Locale; @CommandInfo( id = "hide", aliases = {"modulevisibility", "showmodule"}, - usage = "@hide <module> [on|off|toggle]", + usage = "%hide <module> [on|off|toggle]", descriptionKey = "command.hide.description" ) public final class HideCommand implements ClientCommand { diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/IrisCommand.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/IrisCommand.java index 4a1c9de..2b7e167 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/IrisCommand.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/IrisCommand.java @@ -23,7 +23,7 @@ import java.util.stream.Collectors; @CommandInfo( id = "iris", aliases = "shaderpack", - usage = "@iris", + usage = "%iris", descriptionKey = "command.iris.description" ) public final class IrisCommand implements ClientCommand { diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/ModulesCommand.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/ModulesCommand.java index c340115..71a9e5d 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/ModulesCommand.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/ModulesCommand.java @@ -24,7 +24,7 @@ import java.util.Locale; @CommandInfo( id = "modules", aliases = {"modulelist", "mods"}, - usage = "@modules [all|enabled|combat|movement|player|visuals|misc] [page]", + usage = "%modules [all|enabled|combat|movement|player|visuals|misc] [page]", descriptionKey = "command.modules.description" ) public final class ModulesCommand implements ClientCommand { @@ -54,7 +54,7 @@ public final class ModulesCommand implements ClientCommand { message.append(Component.literal(module.getDisplayName()) .withStyle(style -> style .withColor(color) - .withClickEvent(new ClickEvent.RunCommand("@toggle " + module.name())) + .withClickEvent(new ClickEvent.RunCommand("%toggle " + module.name())) .withHoverEvent(new HoverEvent.ShowText(Component.literal( module.getDescription() + "\nClick to toggle\nID: " + module.name()))))); message.append(Component.literal(" [" + (module.isEnabled() ? "ON" : "OFF") + "]") @@ -105,6 +105,6 @@ public final class ModulesCommand implements ClientCommand { return Component.literal(label).withStyle(style -> style .withBold(true) .withColor(0xFF70E0A0) - .withClickEvent(new ClickEvent.RunCommand("@modules " + filter + " " + page))); + .withClickEvent(new ClickEvent.RunCommand("%modules " + filter + " " + page))); } } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/PingCommand.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/PingCommand.java index ddd3e06..82e4006 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/PingCommand.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/PingCommand.java @@ -16,7 +16,7 @@ import dev.loki.lovisual.util.player.NetworkStatsUtil; @CommandInfo( id = "ping", aliases = "latency", - usage = "@ping", + usage = "%ping", descriptionKey = "command.ping.description" ) public final class PingCommand implements ClientCommand { diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/RelationCommand.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/RelationCommand.java index bd23b17..560c6ad 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/RelationCommand.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/RelationCommand.java @@ -122,7 +122,7 @@ public abstract class RelationCommand implements ClientCommand { @CommandInfo( id = "friend", aliases = {"friends", "f"}, - usage = "@friend [list|add|remove|toggle|clear] [player]", + usage = "%friend [list|add|remove|toggle|clear] [player]", descriptionKey = "command.friend.description" ) public static final class Friends extends RelationCommand { @@ -135,7 +135,7 @@ public abstract class RelationCommand implements ClientCommand { @CommandInfo( id = "enemy", aliases = {"enemies", "e"}, - usage = "@enemy [list|add|remove|toggle|clear] [player]", + usage = "%enemy [list|add|remove|toggle|clear] [player]", descriptionKey = "command.enemy.description" ) public static final class Enemies extends RelationCommand { @@ -148,7 +148,7 @@ public abstract class RelationCommand implements ClientCommand { @CommandInfo( id = "staff", aliases = {"staffs", "admin"}, - usage = "@staff [list|add|remove|toggle|clear] [player]", + usage = "%staff [list|add|remove|toggle|clear] [player]", descriptionKey = "command.staff.description" ) public static final class Staff extends RelationCommand { diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/ServerInfoCommand.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/ServerInfoCommand.java index 5a26254..2e1823b 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/ServerInfoCommand.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/ServerInfoCommand.java @@ -19,7 +19,7 @@ import java.util.Locale; @CommandInfo( id = "serverinfo", aliases = {"server", "sinfo"}, - usage = "@serverinfo", + usage = "%serverinfo", descriptionKey = "command.serverinfo.description" ) public final class ServerInfoCommand implements ClientCommand { diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/ToggleCommand.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/ToggleCommand.java index 6d44138..4fb4fe1 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/ToggleCommand.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/ToggleCommand.java @@ -19,7 +19,7 @@ import java.util.Locale; @CommandInfo( id = "toggle", aliases = "t", - usage = "@toggle <module> [on|off|toggle]", + usage = "%toggle <module> [on|off|toggle]", descriptionKey = "command.toggle.description" ) public final class ToggleCommand implements ClientCommand { diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/TpsCommand.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/TpsCommand.java index 8e59607..43d6343 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/TpsCommand.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/TpsCommand.java @@ -18,7 +18,7 @@ import java.util.Locale; @CommandInfo( id = "tps", aliases = "servertps", - usage = "@tps", + usage = "%tps", descriptionKey = "command.tps.description" ) public final class TpsCommand implements ClientCommand { diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/UsernameCommand.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/UsernameCommand.java index 80d38e4..f6df4ad 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/UsernameCommand.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/UsernameCommand.java @@ -12,10 +12,12 @@ import dev.loki.lovisual.features.command.CommandContext; import dev.loki.lovisual.features.command.CommandInfo; import dev.loki.lovisual.features.command.CommandOutput; +import java.util.List; + @CommandInfo( id = "username", aliases = {"name", "ign", "whoami"}, - usage = "@username", + usage = "%username", descriptionKey = "command.username.description" ) public final class UsernameCommand implements ClientCommand { @@ -29,4 +31,9 @@ public final class UsernameCommand implements ClientCommand { CommandOutput.send("Username: " + profile.name() + " | UUID: " + profile.id()); return true; } + + @Override + public List<String> suggest(CommandContext ctx, int argIndex, String token) { + return List.of(); + } } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/XaeroWaypointCommand.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/XaeroWaypointCommand.java index f1d8234..f963963 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/XaeroWaypointCommand.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/command/impl/XaeroWaypointCommand.java @@ -21,7 +21,7 @@ import java.util.Locale; @CommandInfo( id = "xaero", aliases = {"xmark", "xwp", "waypoint", "marker"}, - usage = "@xaero here [name...] | @xaero [add] <x|~> <y|~> <z|~> [name...] | @xaero remove [all|#|name] | @xaero list", + usage = "%xaero here [name...] | @xaero [add] <x|~> <y|~> <z|~> [name...] | @xaero remove [all|#|name] | @xaero list", descriptionKey = "command.xaero.description" ) public final class XaeroWaypointCommand implements ClientCommand { diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/BetterChatContextMenuHandler.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/BetterChatContextMenuHandler.java index 86f6fbd..22aa778 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/BetterChatContextMenuHandler.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/BetterChatContextMenuHandler.java @@ -6,6 +6,7 @@ */ package dev.loki.lovisual.features.gui.chat; import dev.loki.lovisual.features.command.CommandManager; +import dev.loki.lovisual.features.command.CommandPrefix; import dev.loki.lovisual.features.gui.hud.draggable.impl.BetterChat; import dev.loki.lovisual.mixins.accessors.ChatScreenAccessor; import dev.loki.lovisual.util.screen.ClientScreen; @@ -128,7 +129,7 @@ final class BetterChatContextMenuHandler { } else if (evt instanceof ClickEvent.CopyToClipboard) { ClipboardUtil.copy(val); return true; - } else if (evt instanceof ClickEvent.RunCommand && val.startsWith("@")) { + } else if (evt instanceof ClickEvent.RunCommand && CommandPrefix.isCommandLike(val)) { return CommandManager.handle(val); } else if (evt instanceof ClickEvent.SuggestCommand || evt instanceof ClickEvent.RunCommand) { if (!ctrlDown) return false; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/BetterChatSuggestionRenderer.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/BetterChatSuggestionRenderer.java index e4831d6..2be46b9 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/BetterChatSuggestionRenderer.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/BetterChatSuggestionRenderer.java @@ -5,6 +5,7 @@ * Licensed under the GNU General Public License v3.0. */ package dev.loki.lovisual.features.gui.chat; +import dev.loki.lovisual.features.command.CommandPrefix; import dev.loki.lovisual.mixins.accessors.ChatScreenAccessor; import dev.loki.lovisual.render.helpers.SystemCursor; import net.minecraft.client.Minecraft; @@ -44,7 +45,7 @@ final class BetterChatSuggestionRenderer { windowChanged = CommandSuggestorBridge.peekWindow(BetterChatRenderer.storedScreen) != CommandSuggestorBridge.lastWindow(); } if (inputChanged || windowChanged || BetterChatRenderer.lastSnapshot == null) { - if (prefix == '@') { + if (prefix == CommandPrefix.CHAR || prefix == CommandPrefix.LEGACY) { BetterChatRenderer.lastSnapshot = BetterChatSuggestionState.buildCustomSnapshot(text, cursor); } else { BetterChatRenderer.lastSnapshot = CommandSuggestorBridge.snapshot(BetterChatRenderer.storedScreen); @@ -56,7 +57,7 @@ final class BetterChatSuggestionRenderer { BetterChatSuggestionState.resetSuggestionTracking(); return; } - if (prefix == '@') { + if (prefix == CommandPrefix.CHAR || prefix == CommandPrefix.LEGACY) { BetterChatRenderer.suggestWindow = null; } int hash = snap.texts().hashCode() ^ snap.selection() ^ snap.startIndex(); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/BetterChatSuggestionState.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/BetterChatSuggestionState.java index 2894365..da258cd 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/BetterChatSuggestionState.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/BetterChatSuggestionState.java @@ -8,6 +8,7 @@ package dev.loki.lovisual.features.gui.chat; import dev.loki.lovisual.features.command.CommandManager; +import dev.loki.lovisual.features.command.CommandPrefix; import dev.loki.lovisual.mixins.accessors.ChatScreenAccessor; import dev.loki.lovisual.mixins.accessors.SuggestionWindowAccessor; import dev.loki.lovisual.util.screen.ClientScreen; @@ -80,7 +81,7 @@ final class BetterChatSuggestionState { for (int i = 0; i < text.length(); i++) { char c = text.charAt(i); if (Character.isWhitespace(c)) continue; - if (c == '/' || c == '@') return c; + if (c == '/' || c == CommandPrefix.CHAR || c == CommandPrefix.LEGACY) return c; return 0; } return 0; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/BetterChatTabHandler.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/BetterChatTabHandler.java index f27a5cd..92da8c3 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/BetterChatTabHandler.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/chat/BetterChatTabHandler.java @@ -9,6 +9,7 @@ package dev.loki.lovisual.features.gui.chat; import com.mojang.brigadier.suggestion.Suggestion; import dev.loki.lovisual.features.command.CommandManager; +import dev.loki.lovisual.features.command.CommandPrefix; import dev.loki.lovisual.mixins.accessors.ChatScreenAccessor; import dev.loki.lovisual.util.screen.ClientScreen; import net.minecraft.client.Minecraft; @@ -43,7 +44,7 @@ public final class BetterChatTabHandler { if (field == null) return false; String text = field.getValue(); if (text == null || text.isEmpty()) return false; - if (BetterChatSuggestionState.commandPrefix(text) != '@') return false; + if (BetterChatSuggestionState.commandPrefix(text) != CommandPrefix.CHAR && BetterChatSuggestionState.commandPrefix(text) != CommandPrefix.LEGACY) return false; int cursor = field.getCursorPosition(); List<Suggestion> suggestions = CommandManager.suggest(text, cursor); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/MenuSettingsResolver.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/MenuSettingsResolver.java index a0cb3d2..ebb1db3 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/MenuSettingsResolver.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/MenuSettingsResolver.java @@ -50,7 +50,9 @@ public enum MenuSettingsResolver { new StaticHudCard("vanilla_bar", "Bar"), new StaticHudCard("vanilla_health", "Health"), new StaticHudCard("dynamic_island", "Dynamic Island"), - new StaticHudCard("tab_list", "Tab List") + new StaticHudCard("tab_list", "Tab List"), + new StaticHudCard("reach_display", "Reach"), + new StaticHudCard("watermark", "Watermark") ); private static final SettingOwner RELATIONS_OWNER = new StaticOwner("relations_color", () -> PlayerRelations.get().save()); 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 d697f54..2e1e916 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 @@ -41,7 +41,6 @@ public final class SettingsPanelComponent { private final List<SettingRow> rows = new ArrayList<>(); private final List<SettingHit> hits = new ArrayList<>(); private final SettingRenderSurface renderSurface; - private final Minecraft mc = Minecraft.getInstance(); private String targetId = ""; private String title = "Settings"; private boolean hudContext = false; @@ -487,6 +486,7 @@ public final class SettingsPanelComponent { } private void renderHudEditorHints(float fallbackX, float fallbackY, float fallbackW, float fallbackH, float scale, float alpha) { + Minecraft mc = Minecraft.getInstance(); MainConfig config = MainConfig.get(); if (!hudContext || !config.isClickGuiHudEditorHintsEnabled()) return; @@ -515,6 +515,7 @@ public final class SettingsPanelComponent { } private void renderSettingsPanelHints(float fallbackX, float fallbackY, float fallbackW, float fallbackH, float scale, float alpha) { + Minecraft mc = Minecraft.getInstance(); MainConfig config = MainConfig.get(); if (hudContext || !isActive() || !config.isClickGuiHintsEnabled()) return; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/OnlineRelationPlayerPickerComponent.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/OnlineRelationPlayerPickerComponent.java index 75460ff..b13d1e5 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/OnlineRelationPlayerPickerComponent.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/relations/OnlineRelationPlayerPickerComponent.java @@ -59,7 +59,6 @@ final class OnlineRelationPlayerPickerComponent { private static final long DYNAMIC_REFRESH_MS = 250L; private static final String I18N = "clickgui.settings.relations.online_picker."; - private final Minecraft mc = Minecraft.getInstance(); private final Consumer<String> statusSink; private final List<PlayerRow> rows = new ArrayList<>(); private final List<RowHit> rowHits = new ArrayList<>(); @@ -452,6 +451,7 @@ final class OnlineRelationPlayerPickerComponent { } private void refreshIfNeeded() { + Minecraft mc = Minecraft.getInstance(); if (mc == null || mc.getConnection() == null) return; long now = System.currentTimeMillis(); int onlineCount = onlineCount(); @@ -483,10 +483,12 @@ final class OnlineRelationPlayerPickerComponent { } private int onlineCount() { + Minecraft mc = Minecraft.getInstance(); return mc != null && mc.getConnection() != null ? mc.getConnection().getListedOnlinePlayers().size() : 0; } private void collectRows() { + Minecraft mc = Minecraft.getInstance(); rows.clear(); if (mc == null || mc.getConnection() == null) { rowsVersion++; @@ -520,6 +522,7 @@ final class OnlineRelationPlayerPickerComponent { } private void refreshDynamicRows() { + Minecraft mc = Minecraft.getInstance(); if (mc == null || mc.getConnection() == null || rows.isEmpty()) return; boolean changed = false; for (int i = 0; i < rows.size(); i++) { @@ -546,6 +549,7 @@ final class OnlineRelationPlayerPickerComponent { } private long structureSignature() { + Minecraft mc = Minecraft.getInstance(); if (mc == null || mc.getConnection() == null) return 0L; long h = 0x6A09E667F3BCC909L; for (PlayerInfo info : mc.getConnection().getListedOnlinePlayers()) { @@ -567,6 +571,7 @@ final class OnlineRelationPlayerPickerComponent { } private Component displayName(PlayerInfo info, String fallback) { + Minecraft mc = Minecraft.getInstance(); Component display = null; if (mc != null && mc.gui != null && mc.gui.hud != null && mc.gui.hud.getTabList() != null) { display = mc.gui.hud.getTabList().getNameForDisplay(info); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/theme/ThemeEditorPreviewComponent.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/theme/ThemeEditorPreviewComponent.java index 45f28a9..0109ac4 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/theme/ThemeEditorPreviewComponent.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/clickgui/layout/screen/settings/implement/theme/ThemeEditorPreviewComponent.java @@ -31,7 +31,6 @@ public final class ThemeEditorPreviewComponent { private static final float DESIGN_W = 320f; private static final float DESIGN_H = 190f; - private final Minecraft mc = Minecraft.getInstance(); private final UiScriptModuleHandle previewModuleHandle = HudScriptLayouts.handle(PREVIEW_LAYOUT_ID); private final CachedUiScriptRuntime previewRuntime = new CachedUiScriptRuntime(HudScriptLayouts.runtimeReporter()); private float previewAnim; @@ -93,6 +92,7 @@ public final class ThemeEditorPreviewComponent { float panelH, float unit, float alpha) { + Minecraft mc = Minecraft.getInstance(); if (previewModuleHandle.isRuntimeBlocked() || mc == null || mc.getResourceManager() == null) return; HudScriptLayouts.pollReloadCombo(mc); if (previewModuleHandle.consumeChanged()) { @@ -131,6 +131,7 @@ public final class ThemeEditorPreviewComponent { } private UiScriptModule ensurePreviewModule() { + Minecraft mc = Minecraft.getInstance(); if (!previewModuleHandle.ensureLoaded(mc.getResourceManager())) { HudScriptLayouts.reportLoadError(previewModuleHandle); return null; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/Admins.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/Admins.java index f087e14..df27387 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/Admins.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/Admins.java @@ -72,7 +72,6 @@ public final class Admins extends DraggableHudElement { private static final String EFFECT_NONE = "None"; private static final String EFFECT_BLUR = "Blur"; - private final Minecraft mc = Minecraft.getInstance(); private final HudGlobalConfig hud = HudGlobalConfig.get(); private final ScriptedListHudPanel scriptedPanel = new ScriptedListHudPanel(); private final NumberValue<Double> scaleValue = @@ -176,6 +175,7 @@ public final class Admins extends DraggableHudElement { int screenW, int screenH) { boolean forceVisible = DraggableHudElementRegistry.isForceVisible(); + Minecraft mc = Minecraft.getInstance(); if (mc == null || (mc.player == null && !forceVisible)) { width = 0.0f; height = 0.0f; @@ -450,6 +450,7 @@ public final class Admins extends DraggableHudElement { } private Identifier resolvePreviewSkin() { + Minecraft mc = Minecraft.getInstance(); if (mc != null && mc.player != null) { return PlayerSkinResolver.resolveProfileSkin(mc.player.getGameProfile()); } @@ -609,6 +610,7 @@ public final class Admins extends DraggableHudElement { } private AbstractClientPlayer resolvePlayerByName(String name) { + Minecraft mc = Minecraft.getInstance(); if (name == null || name.isEmpty() || mc == null || mc.level == null) { return null; } @@ -631,6 +633,7 @@ public final class Admins extends DraggableHudElement { return PlayerSkinResolver.resolveProfileSkin(player.getGameProfile()); } + Minecraft mc = Minecraft.getInstance(); if (mc != null && mc.getConnection() != null) { for (PlayerInfo entry : mc.getConnection().getOnlinePlayers()) { var profile = entry.getProfile(); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/Armor.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/Armor.java index f146d9e..b30cb27 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/Armor.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/Armor.java @@ -46,7 +46,6 @@ public final class Armor extends DraggableHudElement { private static final float DEFAULT_MARGIN = 16f; private static final float DIVIDER_THICKNESS = 0.5f; - private final Minecraft mc = Minecraft.getInstance(); private final HudGlobalConfig hud = HudGlobalConfig.get(); private final NumberValue<Double> scaleValue = num("armor_scale", 3.68, 0.5, 5.0); private final EnumValue<LayoutMode> layoutMode = @@ -110,6 +109,7 @@ public final class Armor extends DraggableHudElement { float tickDelta, int screenW, int screenH) { + Minecraft mc = Minecraft.getInstance(); layoutReady = false; boolean preview = DraggableHudElementRegistry.isForceVisible(); float scale = HudScale.scale(screenW, screenH) * scaleValue.get().floatValue(); @@ -187,6 +187,7 @@ public final class Armor extends DraggableHudElement { float tickDelta, int screenW, int screenH) { + Minecraft mc = Minecraft.getInstance(); if (!layoutReady || mc == null || mc.player == null || ctx == null) return; ItemStack[] stacks = cachedStacks != null ? cachedStacks : getArmorStacks(); @@ -216,6 +217,7 @@ public final class Armor extends DraggableHudElement { } private ItemStack[] getArmorStacks() { + Minecraft mc = Minecraft.getInstance(); return new ItemStack[]{ mc.player.getItemBySlot(EquipmentSlot.HEAD), mc.player.getItemBySlot(EquipmentSlot.CHEST), @@ -261,6 +263,7 @@ public final class Armor extends DraggableHudElement { } private boolean shouldShowWhenEmpty() { + Minecraft mc = Minecraft.getInstance(); if (hud == null) return false; HudGlobalConfig.HeaderMode mode = hud.getHeaderMode(); if (mode == HudGlobalConfig.HeaderMode.ALWAYS) return true; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/BetterChat.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/BetterChat.java index 1e3e0b9..d1fb49e 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/BetterChat.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/BetterChat.java @@ -39,7 +39,6 @@ public final class BetterChat extends DraggableHudElement { defaultLayout(16.0f, 485.12f); } - private final Minecraft mc = Minecraft.getInstance(); private final NumberValue<Double> widthRatio = num("width_ratio", 0.54, 0.2, 0.8); private final NumberValue<Double> fontScale = num("font_scale", 1.36, 0.6, 1.4); private final NumberValue<Integer> maxLines = num("lines", 12, 4, 20); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/Cooldowns.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/Cooldowns.java index 3792b40..46bcb9e 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/Cooldowns.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/Cooldowns.java @@ -94,7 +94,6 @@ public final class Cooldowns extends DraggableHudElement { }; private static ItemStack[] previewStacks; - private final Minecraft mc = Minecraft.getInstance(); private final HudGlobalConfig hud = HudGlobalConfig.get(); private final ScriptedListHudPanel scriptedPanel = new ScriptedListHudPanel(); private final NumberValue<Double> scaleValue = @@ -207,6 +206,7 @@ public final class Cooldowns extends DraggableHudElement { float tickDelta, int screenW, int screenH) { + Minecraft mc = Minecraft.getInstance(); foregroundReady = false; pendingItemTasks.clear(); boolean forceVisible = DraggableHudElementRegistry.isForceVisible(); @@ -475,6 +475,7 @@ public final class Cooldowns extends DraggableHudElement { } private List<Row> collectRows() { + Minecraft mc = Minecraft.getInstance(); List<Row> rows = new ArrayList<>(); if (mc != null && mc.player != null) { var manager = mc.player.getCooldowns(); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/HudNotifier.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/HudNotifier.java index fef1ae2..a891581 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/HudNotifier.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/HudNotifier.java @@ -69,7 +69,6 @@ public final class HudNotifier extends DraggableHudElement implements ModuleStat private final ModeValue soundMode = new ModeValue("sound_mode", "2", "1", "2", "3"); private final NumberValue<Double> soundVolume = new NumberValue<>("sound_volume", 0.47, 0.05, 1.0); - private final Minecraft mc = Minecraft.getInstance(); private final List<Toast> toasts = new ArrayList<>(); private final RenderColor colorTmp = new RenderColor(0xFFFFFFFF); private boolean stackLayoutReady; @@ -171,6 +170,7 @@ public final class HudNotifier extends DraggableHudElement implements ModuleStat private void renderEngineInternal(Renderer2D renderer, TextRenderer textRenderer) { + Minecraft mc = Minecraft.getInstance(); if (mc == null || mc.level == null || mc.player == null) return; @@ -497,6 +497,7 @@ public final class HudNotifier extends DraggableHudElement implements ModuleStat } private MessengerSide getSide() { + Minecraft mc = Minecraft.getInstance(); Window window = mc == null ? null : mc.getWindow(); if (window == null) return MessengerSide.RIGHT; int fbw = Math.max(1, window.getWidth()); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/Inventory.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/Inventory.java index a9738fe..1a3c352 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/Inventory.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/Inventory.java @@ -76,7 +76,6 @@ public final class Inventory extends DraggableHudElement { private static final String EFFECT_NONE = "None"; private static final String EFFECT_BLUR = "Blur"; - private final Minecraft mc = Minecraft.getInstance(); private final HudGlobalConfig hud = HudGlobalConfig.get(); private final ScriptedInventoryHudPanel scriptedPanel = new ScriptedInventoryHudPanel(); @@ -170,6 +169,7 @@ public final class Inventory extends DraggableHudElement { @Override public void onTick() { + Minecraft mc = Minecraft.getInstance(); if (!toggleInit) { if (mc == null || mc.getWindow() == null) { return; @@ -204,6 +204,7 @@ public final class Inventory extends DraggableHudElement { float tickDelta, int screenW, int screenH) { + Minecraft mc = Minecraft.getInstance(); layoutReady = false; hitboxActive = false; pendingItemTasks.clear(); @@ -374,6 +375,7 @@ public final class Inventory extends DraggableHudElement { } private List<ItemStack> collectStacks() { + Minecraft mc = Minecraft.getInstance(); List<ItemStack> stacks = new ArrayList<>(LAST_INV_SLOT - FIRST_INV_SLOT); if (mc == null || mc.player == null) { return stacks; @@ -450,6 +452,7 @@ public final class Inventory extends DraggableHudElement { } private void initToggleState() { + Minecraft mc = Minecraft.getInstance(); toggleArmed = true; String combo = toggleBind.get(); if (combo != null && !combo.isBlank() && !"NONE".equalsIgnoreCase(combo) diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/Itemizer.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/Itemizer.java index 5ba5202..f2e20ba 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/Itemizer.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/Itemizer.java @@ -54,7 +54,6 @@ public final class Itemizer extends DraggableHudElement { public static Itemizer INSTANCE; - private final Minecraft mc = Minecraft.getInstance(); private final UiScriptModuleHandle moduleHandle = HudScriptLayouts.handle(LAYOUT_ID); private final CachedUiScriptRuntime runtime = new CachedUiScriptRuntime(HudScriptLayouts.runtimeReporter()); private final LinkedHashMap<ItemizerEvent, Entry> entries = new LinkedHashMap<>(); @@ -334,6 +333,7 @@ public final class Itemizer extends DraggableHudElement { } private UiScriptModule ensureModule() { + Minecraft mc = Minecraft.getInstance(); if (mc == null || mc.getResourceManager() == null) return null; HudScriptLayouts.pollReloadCombo(mc); if (moduleHandle.consumeChanged()) { diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/MediaPlayer.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/MediaPlayer.java index 3df3ff4..db6f3eb 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/MediaPlayer.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/MediaPlayer.java @@ -72,7 +72,6 @@ public final class MediaPlayer extends DraggableHudElement { private static final int ICON_NEXT = 0xEA05; private static final int ICON_SHUFFLE = 0xEA06; - private final Minecraft mc = Minecraft.getInstance(); private final HudGlobalConfig hud = HudGlobalConfig.get(); private final MediaSessionService mediaService = MediaSessionService.get(); private final NumberValue<Double> scale = @@ -209,6 +208,7 @@ public final class MediaPlayer extends DraggableHudElement { float tickDelta, int screenW, int screenH) { + Minecraft mc = Minecraft.getInstance(); if (mc == null) return; boolean chatOpen = ClientScreen.current() instanceof ChatScreen; boolean preview = DraggableHudElementRegistry.isForceVisible(); @@ -539,6 +539,7 @@ public final class MediaPlayer extends DraggableHudElement { } private void updateHover(boolean chatOpen, boolean showShuffle, boolean showRepeat) { + Minecraft mc = Minecraft.getInstance(); if (!chatOpen) { playHover = lerp(playHover, 0f, 0.2f); shuffleHover = lerp(shuffleHover, 0f, 0.2f); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/Radar.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/Radar.java index 9f38483..feef963 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/Radar.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/Radar.java @@ -56,7 +56,6 @@ public final class Radar extends DraggableHudElement { Field.PING, Field.NONE }; - private final Minecraft mc = Minecraft.getInstance(); private final HudGlobalConfig hud = HudGlobalConfig.get(); private final EnumValue<Mode> mode = new EnumValue<>("radar_mode", Mode.RECT, Mode.RECT, Mode.TEXT); @@ -135,6 +134,7 @@ public final class Radar extends DraggableHudElement { float tickDelta, int screenW, int screenH) { + Minecraft mc = Minecraft.getInstance(); boolean preview = DraggableHudElementRegistry.isForceVisible(); if (mc == null && !preview) { width = 0f; @@ -195,6 +195,7 @@ public final class Radar extends DraggableHudElement { } private void renderBlips(Renderer2D renderer, float tickDelta, float cx, float cy, float half, float scale) { + Minecraft mc = Minecraft.getInstance(); if (mc == null || mc.player == null || mc.level == null) return; float blip = Math.max(2.5f, BLIP_SIZE * scale); @@ -225,6 +226,7 @@ public final class Radar extends DraggableHudElement { } private void renderTextMode(TextRenderer textRenderer, int screenW, int screenH, boolean preview) { + Minecraft mc = Minecraft.getInstance(); boolean ready = mc != null && mc.player != null && mc.level != null; if (!ready && !preview) { width = 0f; @@ -290,6 +292,7 @@ public final class Radar extends DraggableHudElement { } private void appendField(StringBuilder out, Field field, Player player) { + Minecraft mc = Minecraft.getInstance(); if (field == null || field == Field.NONE) return; if (out.length() > 0) out.append(' '); switch (field) { @@ -317,6 +320,7 @@ public final class Radar extends DraggableHudElement { } private int getPlayerPing(Player player) { + Minecraft mc = Minecraft.getInstance(); if (mc == null || mc.getConnection() == null) return -1; PlayerInfo entry = mc.getConnection().getPlayerInfo(player.getUUID()); return entry != null ? entry.getLatency() : -1; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/Scoreboard.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/Scoreboard.java index a19b529..2d5f939 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/Scoreboard.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/Scoreboard.java @@ -77,7 +77,6 @@ public final class Scoreboard extends DraggableHudElement { private static final String EFFECT_NONE = "None"; private static final String EFFECT_BLUR = "Blur"; - private final Minecraft mc = Minecraft.getInstance(); private final HudGlobalConfig hud = HudGlobalConfig.get(); private final KeyBindValue toggleBind = @@ -582,6 +581,7 @@ public final class Scoreboard extends DraggableHudElement { @Override public void onTick() { + Minecraft mc = Minecraft.getInstance(); if (!toggleInit) { if (mc == null || mc.getWindow() == null) { return; @@ -616,6 +616,7 @@ public final class Scoreboard extends DraggableHudElement { float tickDelta, int screenW, int screenH) { + Minecraft mc = Minecraft.getInstance(); boolean preview = DraggableHudElementRegistry.isForceVisible(); if (mc == null || (mc.level == null && !preview)) { width = 0.0f; @@ -758,6 +759,7 @@ public final class Scoreboard extends DraggableHudElement { } private Objective resolveObjective() { + Minecraft mc = Minecraft.getInstance(); if (mc == null || mc.level == null || mc.player == null) { return null; } @@ -824,6 +826,7 @@ public final class Scoreboard extends DraggableHudElement { } private void initToggleState() { + Minecraft mc = Minecraft.getInstance(); toggleArmed = true; String combo = toggleBind.get(); if (combo != null && !combo.isBlank() && !"NONE".equalsIgnoreCase(combo) diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/SpeedBps.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/SpeedBps.java index 53daf24..8e74c8c 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/SpeedBps.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/SpeedBps.java @@ -64,7 +64,6 @@ public final class SpeedBps extends DraggableHudElement implements ScriptableHud private static final long SAMPLE_INTERVAL_MS = 75L; private static final int SAMPLE_COUNT = 20; - private final Minecraft mc = Minecraft.getInstance(); private final HudGlobalConfig hud = HudGlobalConfig.get(); private final BooleanValue useBps = bool("speed_bps_bps", true); private final BooleanValue average = bool("speed_bps_average", false); @@ -140,6 +139,7 @@ public final class SpeedBps extends DraggableHudElement implements ScriptableHud float tickDelta, int screenW, int screenH) { + Minecraft mc = Minecraft.getInstance(); boolean preview = DraggableHudElementRegistry.isForceVisible(); if (mc == null && !preview) { width = 0f; @@ -252,6 +252,7 @@ public final class SpeedBps extends DraggableHudElement implements ScriptableHud } private float currentBps() { + Minecraft mc = Minecraft.getInstance(); return PlayerSpeedUtil.getBps(mc != null ? mc.player : null); } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/TargetHud.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/TargetHud.java index 34b6aec..daef5b3 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/TargetHud.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/TargetHud.java @@ -17,7 +17,6 @@ import dev.loki.lovisual.render.engine.math.HudScale; import dev.loki.lovisual.render.engine.renderer.Renderer2D; import dev.loki.lovisual.render.engine.text.TextRenderer; import dev.loki.lovisual.util.pvp.opponents.TotemPopCounter; -import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.LivingEntity; @@ -119,7 +118,6 @@ public final class TargetHud extends DraggableHudElement { static final ConcurrentHashMap<Integer, Long> HIT_FLASH_MS_BY_ID = new ConcurrentHashMap<>(); - final Minecraft mc = Minecraft.getInstance(); final HudGlobalConfig hud = HudGlobalConfig.get(); final NumberValue<Double> scale = diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/TargetHudContentRenderer.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/TargetHudContentRenderer.java index 56a892e..aee4b15 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/TargetHudContentRenderer.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/TargetHudContentRenderer.java @@ -15,6 +15,7 @@ import dev.loki.lovisual.render.engine.text.FontInfo; import dev.loki.lovisual.render.engine.text.Fonts; import dev.loki.lovisual.render.engine.text.TextRenderer; import dev.loki.lovisual.util.player.PlayerHealthResolver; +import net.minecraft.client.Minecraft; import net.minecraft.world.entity.LivingEntity; import static dev.loki.lovisual.features.gui.hud.draggable.impl.TargetHud.*; @@ -36,6 +37,7 @@ final class TargetHudContentRenderer { float alphaFactor, long now, float dt) { + Minecraft mc = Minecraft.getInstance(); float contentX = x + (FACE_X + FACE_SIZE + CONTENT_GAP + CONTENT_X_OFFSET) * scaleFactor; float nameY = y + (NAME_Y + CONTENT_Y_OFFSET) * scaleFactor; @@ -63,9 +65,9 @@ final class TargetHudContentRenderer { element.absorptionAnimation = AnimationUtility.approach(element.absorptionAnimation, targetAbsorptionFraction, dt, BAR_SPEED); element.absorptionAnimation = AnimationUtility.snap(element.absorptionAnimation, targetAbsorptionFraction, PROGRESS_SNAP_EPSILON); - boolean selfTarget = target == element.mc.player; + boolean selfTarget = target == mc.player; if (!selfTarget) { - float targetDistance = element.mc.player != null ? element.mc.player.distanceTo(target) : 0.0f; + float targetDistance = mc.player != null ? mc.player.distanceTo(target) : 0.0f; element.displayedDistance = AnimationUtility.approach(element.displayedDistance, targetDistance, dt, DISTANCE_SPEED); element.displayedDistance = AnimationUtility.snap(element.displayedDistance, targetDistance, 0.01f); } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/TargetHudEffectsRenderer.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/TargetHudEffectsRenderer.java index 1e57c1b..7800527 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/TargetHudEffectsRenderer.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/TargetHudEffectsRenderer.java @@ -14,6 +14,7 @@ import dev.loki.lovisual.render.engine.text.FontInfo; import dev.loki.lovisual.render.engine.text.Fonts; import dev.loki.lovisual.render.engine.text.TextRenderer; import dev.loki.lovisual.util.player.effect.StatusEffectView; +import net.minecraft.client.Minecraft; import net.minecraft.resources.Identifier; import net.minecraft.world.effect.MobEffectInstance; import net.minecraft.world.entity.LivingEntity; @@ -141,14 +142,15 @@ final class TargetHudEffectsRenderer { } private static List<MobEffectInstance> collectTargetEffects(TargetHud element, LivingEntity target, boolean preview) { + Minecraft mc = Minecraft.getInstance(); if (target == null) { return List.of(); } - if (target == element.mc.player && !element.effectsSelf.get() && !preview) { + if (target == mc.player && !element.effectsSelf.get() && !preview) { return List.of(); } if (target instanceof Player player) { - StatusEffectView.sync(element.mc); + StatusEffectView.sync(mc); List<MobEffectInstance> effects = StatusEffectView.collectHudEffects(player); if (!effects.isEmpty() || !preview) { return effects; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/TargetHudFaceRenderer.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/TargetHudFaceRenderer.java index aee167a..ab0e65e 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/TargetHudFaceRenderer.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/TargetHudFaceRenderer.java @@ -17,6 +17,7 @@ import dev.loki.lovisual.render.engine.text.Fonts; import dev.loki.lovisual.render.engine.text.TextRenderer; import dev.loki.lovisual.util.player.PlayerSkinResolver; import dev.loki.lovisual.util.pvp.opponents.TotemPopSnapshot; +import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.resources.Identifier; import net.minecraft.world.entity.LivingEntity; @@ -112,6 +113,7 @@ final class TargetHudFaceRenderer { } static void drawPlaceholderFace(TargetHud element, Renderer2D renderer, TextRenderer textRenderer, LivingEntity target, float x, float y, float size, float radius, float alphaFactor, float hurtPercent) { + Minecraft mc = Minecraft.getInstance(); int light = HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(element.uiBgSecondary, element.uiHealthEnd, 0.28f), alphaFactor); int dark = HudRenderUtil.scaleAlpha(HudRenderUtil.mixColor(element.uiBgPrimary, 0xFF000000, 0.22f), alphaFactor); renderer.roundedRectGradient(x, y, size, size, radius, 1.0f, light, dark, 90.0f); @@ -125,8 +127,8 @@ final class TargetHudFaceRenderer { String letter = name.isEmpty() ? "?" : name.substring(0, 1).toUpperCase(Locale.ROOT); TextRenderer faceRenderer = Fonts.renderer("Inter", FontInfo.Type.Bold, textRenderer); float textScale = PLACEHOLDER_TEXT_SCALE * element.scale.get().floatValue() * HudScale.scale( - element.mc.getWindow().getGuiScaledWidth(), - element.mc.getWindow().getGuiScaledHeight() + mc.getWindow().getGuiScaledWidth(), + mc.getWindow().getGuiScaledHeight() ); faceRenderer.begin(textScale, false, false); try { diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/TargetHudRenderer.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/TargetHudRenderer.java index e3e2ee6..57ba761 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/TargetHudRenderer.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/TargetHudRenderer.java @@ -18,6 +18,7 @@ import dev.loki.lovisual.render.engine.profiler.RenderProfiler2D; import dev.loki.lovisual.render.engine.renderer.Renderer2D; import dev.loki.lovisual.render.engine.text.TextRenderer; import dev.loki.lovisual.util.pvp.opponents.TotemPopSnapshot; +import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.world.entity.LivingEntity; @@ -30,10 +31,11 @@ final class TargetHudRenderer { } static void render(TargetHud element, Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta, int screenW, int screenH) { + Minecraft mc = Minecraft.getInstance(); try (ProfilerPhase.Scope ignored = ProfilerPhase.scope("2d:target_hud:main"); RenderProfiler2D.Section ignoredSection = RenderProfiler2D.section("target_hud:main")) { boolean preview = DraggableHudElementRegistry.isForceVisible(); - if (element.mc == null || (element.mc.player == null && !preview)) return; + if (mc == null || (mc.player == null && !preview)) return; long now = System.currentTimeMillis(); LivingEntity target = TargetHudTargetResolver.resolveTarget(element, preview); TargetHudTargetResolver.updateDisplayTarget(element, target, now); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/TargetHudTargetResolver.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/TargetHudTargetResolver.java index 55ca780..6c1ec30 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/TargetHudTargetResolver.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/draggable/impl/TargetHudTargetResolver.java @@ -12,6 +12,7 @@ import dev.loki.lovisual.util.pvp.opponents.TotemPopCounter; import dev.loki.lovisual.util.pvp.opponents.TotemPopSnapshot; import dev.loki.lovisual.util.screen.ClientScreen; import dev.loki.lovisual.util.target.TargetManager; +import net.minecraft.client.Minecraft; import net.minecraft.client.gui.screens.ChatScreen; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.player.Player; @@ -22,17 +23,19 @@ final class TargetHudTargetResolver { } static LivingEntity resolveTarget(TargetHud element, boolean preview) { + Minecraft mc = Minecraft.getInstance(); LivingEntity target = TargetManager.getTarget(); if (target != null && element.playersOnly.get() && !(target instanceof Player)) { target = null; } if (target == null && (preview || ClientScreen.current() instanceof ChatScreen)) { - target = element.mc.player; + target = mc.player; } return target; } static void updateDisplayTarget(TargetHud element, LivingEntity target, long now) { + Minecraft mc = Minecraft.getInstance(); if (target != null) { if (element.displayTarget != target) { element.displayTarget = target; @@ -43,14 +46,14 @@ final class TargetHudTargetResolver { element.healthAnimation = realHealth / baseMaxHealth; element.trailAnimation = element.healthAnimation; element.absorptionAnimation = health.absorption() / baseMaxHealth; - element.displayedDistance = element.mc.player != null ? element.mc.player.distanceTo(target) : 0.0f; + element.displayedDistance = mc.player != null ? mc.player.distanceTo(target) : 0.0f; element.waveStartMs = now; } element.lastSeenMs = now; return; } - if (element.displayTarget == element.mc.player && !(ClientScreen.current() instanceof ChatScreen)) { + if (element.displayTarget == mc.player && !(ClientScreen.current() instanceof ChatScreen)) { element.lastSeenMs = 0L; } } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/DynamicIsland.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/DynamicIsland.java index 1125ee5..61957ad 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/DynamicIsland.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/DynamicIsland.java @@ -100,7 +100,6 @@ public final class DynamicIsland extends AbstractHudElement { private static final int STATUS_PVP = 0xFFFF6368; private static final int PROGRESS_BG = 0x2AFFFFFF; - private final Minecraft mc = Minecraft.getInstance(); private final MediaSessionService mediaService = MediaSessionService.get(); private final BooleanValue syncTheme = new BooleanValue("dynamic_island_sync_theme", true); private final NumberValue<Integer> themeAlpha = new NumberValue<>("dynamic_island_theme_alpha", 200, 0, 255); @@ -189,11 +188,13 @@ public final class DynamicIsland extends AbstractHudElement { @Override public boolean isMouseOverInteractive(float mx, float my) { + Minecraft mc = Minecraft.getInstance(); return mc != null && isInteractiveScreen(ClientScreen.current()) && contains(mx, my); } @Override public boolean onMouseClicked(float mx, float my, int button) { + Minecraft mc = Minecraft.getInstance(); if (mc == null || !isInteractiveScreen(ClientScreen.current())) return false; if (!contains(mx, my)) return false; @@ -241,6 +242,7 @@ public final class DynamicIsland extends AbstractHudElement { } private void renderScreenOverlayInternal(GuiGraphicsExtractor ctx, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); if (!Lifecycle.canRunHud()) return; if (mc == null || mc.getWindow() == null || ctx == null) return; Screen currentScreen = ClientScreen.current(); @@ -275,6 +277,7 @@ public final class DynamicIsland extends AbstractHudElement { int screenW, int screenH, boolean screenOverlayPass) { + Minecraft mc = Minecraft.getInstance(); if (mc == null) return; HudScriptLayouts.pollReloadCombo(mc); if (moduleHandle.consumeChanged()) { @@ -562,6 +565,7 @@ public final class DynamicIsland extends AbstractHudElement { } private UiScriptModule ensureModule() { + Minecraft mc = Minecraft.getInstance(); if (mc == null || mc.getResourceManager() == null) return null; if (!moduleHandle.ensureLoaded(mc.getResourceManager())) { HudScriptLayouts.reportLoadError(moduleHandle); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/ReachDisplay.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/ReachDisplay.java new file mode 100644 index 0000000..5e1a400 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/ReachDisplay.java @@ -0,0 +1,140 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.nondraggable.impl; + +import dev.loki.lovisual.config.values.BooleanValue; +import dev.loki.lovisual.config.values.NumberValue; +import dev.loki.lovisual.config.values.RGBColorValue; +import dev.loki.lovisual.features.gui.hud.AbstractHudElement; +import dev.loki.lovisual.features.gui.hud.HudElementRegister; +import dev.loki.lovisual.features.gui.hud.HudRenderSpace; +import dev.loki.lovisual.features.gui.hud.HudRenderUtil; +import dev.loki.lovisual.render.engine.color.RenderColor; +import dev.loki.lovisual.render.engine.math.HudScale; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.FontInfo; +import dev.loki.lovisual.render.engine.text.Fonts; +import dev.loki.lovisual.render.engine.text.TextRenderer; +import dev.loki.lovisual.util.combat.ReachMeter; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; + +import static dev.loki.lovisual.features.theme.Theme.theme; + +/** + * Shows the reach (eye -> target hitbox distance) of your last melee hit just + * below the crosshair, then fades out. Pure information widget: it never + * changes attack range. + */ +@HudElementRegister(order = 35) +public final class ReachDisplay extends AbstractHudElement { + + public static final ReachDisplay INSTANCE = new ReachDisplay(); + + private static final float PAD_X = 6f; + private static final float PAD_Y = 4f; + private static final float RADIUS = 5f; + private static final float CROSSHAIR_GAP = 26f; + private static final float FADE_WINDOW_MS = 1800f; + private static final int OVERREACH_TINT = 0xFFFF6368; + + private final NumberValue<Double> scale = num("reach_scale", "scale", 1.0, 0.5, 2.5); + private final NumberValue<Integer> keepMs = num("reach_keep_ms", "keep_ms", 2500, 500, 10000); + private final BooleanValue syncTheme = bool("reach_sync_theme", "sync_theme", true); + private final BooleanValue showMax = bool("reach_show_max", "show_max", true); + private final BooleanValue highlightOverreach = bool("reach_highlight_overreach", "highlight_overreach", true); + private final RGBColorValue valueColor = + visibleWhen(colorNoAlpha("reach_value_color", "#5CC8E7"), () -> !syncTheme.get()); + private final RGBColorValue bgColor = + visibleWhen(colorNoAlpha("reach_bg_color", "#1A1D23"), () -> !syncTheme.get()); + + private ReachDisplay() { + super("reach_display", "Reach", true); + } + + public static ReachDisplay get() { + return INSTANCE; + } + + @Override + public boolean usesEngineRenderer() { + return true; + } + + @Override + public HudRenderSpace getRenderSpace() { + return HudRenderSpace.UNSCALED_LOGICAL; + } + + @Override + public void renderEngine(Renderer2D renderer, + TextRenderer textRenderer, + GuiGraphicsExtractor ctx, + float tickDelta, + int screenW, + int screenH) { + double distance = ReachMeter.lastDistance(); + Minecraft mc = Minecraft.getInstance(); + if (distance < 0.0 || mc == null || mc.player == null || mc.level == null) { + width = 0f; + height = 0f; + return; + } + + long age = System.currentTimeMillis() - ReachMeter.lastAttackAtMs(); + if (age > keepMs.get() + FADE_WINDOW_MS) { + width = 0f; + height = 0f; + return; + } + int alpha = clampAlpha((int) ((1.0f - Math.max(0f, age - keepMs.get()) / FADE_WINDOW_MS) * 255f)); + + String value = ReachMeter.format(distance); + if (showMax.get()) { + value = value + " • max " + ReachMeter.format(ReachMeter.sessionMax()); + } + + float drawScale = HudScale.scale(screenW, screenH) * scale.get().floatValue(); + TextRenderer fallback = textRenderer != null ? textRenderer : TextRenderer.get(); + TextRenderer font = Fonts.renderer("OnestMedium", FontInfo.Type.Regular, fallback); + + font.begin(drawScale, true, false); + float textW = (float) font.getWidth(value, false); + float textH = (float) font.getHeight(false); + font.end(); + + float boxW = textW + PAD_X * 2f * drawScale; + float boxH = textH + PAD_Y * 2f * drawScale; + float boxX = (screenW - boxW) * 0.5f; + float boxY = screenH * 0.5f + CROSSHAIR_GAP * drawScale; + + boolean overreach = highlightOverreach.get() && distance > ReachMeter.VANILLA_REACH; + int valueArgb = overreach ? OVERREACH_TINT + : syncTheme.get() ? theme().accent() : valueColor.getArgb() | 0xFF000000; + int bg = syncTheme.get() + ? HudRenderUtil.setAlpha(theme().windowBg(), 0xCC) + : (0xCC << 24) | (bgColor.getArgb() & 0x00FFFFFF); + + renderer.roundedRect(boxX, boxY, boxW, boxH, RADIUS * drawScale, 1.0f, + HudRenderUtil.setAlpha(bg, alpha)); + + font.begin(drawScale, true, false); + font.render(value, boxX + PAD_X * drawScale, boxY + (boxH - textH) * 0.5f, + new RenderColor(HudRenderUtil.setAlpha(valueArgb, alpha)), false); + font.end(); + + width = boxW; + height = boxH; + x = boxX; + y = boxY; + } + + private static int clampAlpha(int a) { + return Math.max(0, Math.min(255, a)); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/Watermark.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/Watermark.java new file mode 100644 index 0000000..bbe5f6a --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/Watermark.java @@ -0,0 +1,178 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.gui.hud.nondraggable.impl; + +import dev.loki.lovisual.config.values.BooleanValue; +import dev.loki.lovisual.config.values.NumberValue; +import dev.loki.lovisual.config.values.RGBColorValue; +import dev.loki.lovisual.features.gui.hud.AbstractHudElement; +import dev.loki.lovisual.features.gui.hud.HudElementRegister; +import dev.loki.lovisual.features.gui.hud.HudRenderSpace; +import dev.loki.lovisual.features.gui.hud.HudRenderUtil; +import dev.loki.lovisual.render.engine.color.RenderColor; +import dev.loki.lovisual.render.engine.math.HudScale; +import dev.loki.lovisual.render.engine.renderer.Renderer2D; +import dev.loki.lovisual.render.engine.text.FontInfo; +import dev.loki.lovisual.render.engine.text.Fonts; +import dev.loki.lovisual.render.engine.text.TextRenderer; +import dev.loki.lovisual.util.FastFps; +import dev.loki.lovisual.util.player.NetworkStatsUtil; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; + +import static dev.loki.lovisual.features.theme.Theme.theme; + +/** + * LoVisual watermark: a small brand pill in the top-left corner with optional + * FPS and ping segments. Static UI element (Settings -> UI), theme-synced colors, + * not a copy of the SoupVisuals component stack. + */ +@HudElementRegister(order = 30) +public final class Watermark extends AbstractHudElement { + + public static final Watermark INSTANCE = new Watermark(); + + private static final float PAD_X = 6f; + private static final float PAD_Y = 4f; + private static final float GAP = 5f; + private static final float DOT_SIZE = 3f; + private static final float RADIUS = 5f; + private static final float ORIGIN_X = 4f; + private static final float ORIGIN_Y = 4f; + + private final NumberValue<Double> scale = num("watermark_scale", "scale", 1.0, 0.5, 2.5); + private final BooleanValue syncTheme = bool("watermark_sync_theme", "sync_theme", true); + private final BooleanValue showVersion = bool("watermark_show_version", "show_version", true); + private final BooleanValue showUser = bool("watermark_show_user", "show_user", false); + private final BooleanValue showFps = bool("watermark_show_fps", "show_fps", true); + private final BooleanValue showPing = bool("watermark_show_ping", "show_ping", true); + private final RGBColorValue titleColor = + visibleWhen(colorNoAlpha("watermark_title_color", "title_color", "#5CC8E7"), () -> !syncTheme.get()); + private final RGBColorValue metaColor = + visibleWhen(colorNoAlpha("watermark_meta_color", "meta_color", "#9B9B9B"), () -> !syncTheme.get()); + private final RGBColorValue bgColor = + visibleWhen(colorNoAlpha("watermark_bg_color", "bg_color", "#1A1D23"), () -> !syncTheme.get()); + + private Watermark() { + super("watermark", "Watermark", true); + } + + public static Watermark get() { + return INSTANCE; + } + + static String brandLine() { + return FabricLoader.getInstance().getModContainer("lovisual") + .map(container -> container.getMetadata().getVersion().getFriendlyString()) + .orElse(""); + } + + @Override + public boolean usesEngineRenderer() { + return true; + } + + @Override + public HudRenderSpace getRenderSpace() { + return HudRenderSpace.UNSCALED_LOGICAL; + } + + @Override + public void renderEngine(Renderer2D renderer, + TextRenderer textRenderer, + GuiGraphicsExtractor ctx, + float tickDelta, + int screenW, + int screenH) { + if (!isEnabled()) return; + + Minecraft mc = Minecraft.getInstance(); + String brand = "LoVisual"; + String version = showVersion.get() ? brandLine() : ""; + String fps = showFps.get() ? Math.max(0, FastFps.getFps()) + " fps" : null; + String ping = null; + if (showPing.get() && mc != null) { + int pingMs = NetworkStatsUtil.getPing(mc); + if (pingMs >= 0) ping = pingMs + " ms"; + } + + String meta = joinMeta( + version, + showUser.get() && mc != null && mc.getUser() != null ? mc.getUser().getName() : null, + fps, + ping); + float drawScale = HudScale.scale(screenW, screenH) * scale.get().floatValue(); + + TextRenderer fallback = textRenderer != null ? textRenderer : TextRenderer.get(); + TextRenderer titleFont = Fonts.renderer("OnestMedium", FontInfo.Type.Bold, fallback); + TextRenderer metaFont = Fonts.renderer("OnestMedium", FontInfo.Type.Regular, fallback); + + titleFont.begin(drawScale, true, false); + float brandW = (float) titleFont.getWidth(brand, false); + float textH = (float) titleFont.getHeight(false); + titleFont.end(); + + float metaW = 0f; + if (!meta.isEmpty()) { + metaFont.begin(drawScale, true, false); + metaW = (float) metaFont.getWidth(meta, false); + metaFont.end(); + } + + float dotW = meta.isEmpty() ? 0f : DOT_SIZE * drawScale + GAP * drawScale; + float boxW = (brandW + dotW + metaW) + PAD_X * 2f * drawScale; + float boxH = textH + PAD_Y * 2f * drawScale; + float radius = RADIUS * drawScale; + + int bg = resolveBg(); + int titleArgb = syncTheme.get() ? theme().accent() : titleColor.getArgb() | 0xFF000000; + int metaArgb = syncTheme.get() ? theme().textMuted() : metaColor.getArgb() | 0xFF000000; + + renderer.roundedRect(ORIGIN_X, ORIGIN_Y, boxW, boxH, radius, 1.0f, bg); + + float cursorX = ORIGIN_X + PAD_X * drawScale; + float textY = ORIGIN_Y + (boxH - textH) * 0.5f; + + titleFont.begin(drawScale, true, false); + titleFont.render(brand, cursorX, textY, new RenderColor(titleArgb), false); + titleFont.end(); + cursorX += brandW; + + if (!meta.isEmpty()) { + float dotY = textY + textH * 0.5f - DOT_SIZE * drawScale * 0.5f; + renderer.roundedRect(cursorX + GAP * drawScale * 0.5f, dotY, + DOT_SIZE * drawScale, DOT_SIZE * drawScale, + DOT_SIZE * drawScale * 0.5f, 1.0f, HudRenderUtil.setAlpha(metaArgb, 0xB0)); + cursorX += dotW; + metaFont.begin(drawScale, true, false); + metaFont.render(meta, cursorX, textY, new RenderColor(metaArgb), false); + metaFont.end(); + } + + width = boxW; + height = boxH; + } + + static String joinMeta(String... parts) { + StringBuilder sb = new StringBuilder(); + for (String part : parts) { + if (part == null || part.isBlank()) continue; + if (!sb.isEmpty()) sb.append(" "); + sb.append(part); + } + return sb.toString(); + } + + private int resolveBg() { + if (syncTheme.get()) { + return HudRenderUtil.setAlpha(theme().windowBg(), 0xCC); + } + return (0xCC << 24) | (bgColor.getArgb() & 0x00FFFFFF); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/CustomTabList.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/CustomTabList.java index d6da455..fa674e3 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/CustomTabList.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/nondraggable/impl/tab/CustomTabList.java @@ -57,7 +57,6 @@ public final class CustomTabList extends AbstractHudElement { private static final long STRUCTURE_PROBE_INTERVAL_MS = 250L; private static final long DYNAMIC_REFRESH_INTERVAL_MS = 500L; - private final Minecraft mc = Minecraft.getInstance(); private final TabListAnimator animator = new TabListAnimator(); private final NumberValue<Integer> maxWidth = num("tab_list_max_width", "max_width", 1680, 760, 2600); private final BooleanValue syncTheme = bool("tab_list_sync_theme", "sync_theme", true); @@ -107,6 +106,7 @@ public final class CustomTabList extends AbstractHudElement { } private void renderVanillaTabStratumInternal(GuiGraphicsExtractor ctx, int guiWidth) { + Minecraft mc = Minecraft.getInstance(); if (!Lifecycle.canRunHud()) return; if (mc == null || mc.getWindow() == null || ctx == null) return; int screenW = Math.max(1, Math.round(HudScale.virtualWidth( @@ -170,6 +170,7 @@ public final class CustomTabList extends AbstractHudElement { int screenW, int screenH, boolean show) { + Minecraft mc = Minecraft.getInstance(); if (mc == null || ctx == null || renderer == null) return; float panelPresence = animator.updatePanel(show); if (!show && panelPresence <= 0.01f && !animator.hasVisibleRows()) { @@ -196,6 +197,7 @@ public final class CustomTabList extends AbstractHudElement { } private void refreshSnapshot(int screenW, boolean force) { + Minecraft mc = Minecraft.getInstance(); if (mc == null || mc.getConnection() == null) return; long now = Util.getMillis(); int listedPlayers = Math.min(mc.getConnection().getListedOnlinePlayers().size(), TabListModel.maxPlayers()); @@ -239,10 +241,12 @@ public final class CustomTabList extends AbstractHudElement { } private boolean canRenderReplacement() { + Minecraft mc = Minecraft.getInstance(); return mc != null && mc.player != null && mc.getConnection() != null; } private boolean shouldShowNow() { + Minecraft mc = Minecraft.getInstance(); if (mc == null || mc.player == null || mc.getConnection() == null || mc.options == null) return false; if (!mc.options.keyPlayerList.isDown()) return false; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/script/ScriptedCompactHudStatRenderer.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/script/ScriptedCompactHudStatRenderer.java index c2f0473..cc45ae9 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/script/ScriptedCompactHudStatRenderer.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/gui/hud/script/ScriptedCompactHudStatRenderer.java @@ -23,7 +23,6 @@ public final class ScriptedCompactHudStatRenderer { private static final String LAYOUT_ID = "lovisual:api/hud/draggable/compact_stat"; - private final Minecraft mc = Minecraft.getInstance(); private final UiScriptModuleHandle moduleHandle = HudScriptLayouts.handle(LAYOUT_ID); private final CachedUiScriptRuntime scriptRuntime = new CachedUiScriptRuntime(HudScriptLayouts.runtimeReporter()); @@ -35,6 +34,7 @@ public final class ScriptedCompactHudStatRenderer { TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); if (model == null || !model.visible()) return false; if (moduleHandle.isRuntimeBlocked()) return false; if (mc == null || mc.getResourceManager() == null) return false; @@ -73,6 +73,7 @@ public final class ScriptedCompactHudStatRenderer { } private UiScriptModule ensureModule() { + Minecraft mc = Minecraft.getInstance(); if (!moduleHandle.ensureLoaded(mc.getResourceManager())) { HudScriptLayouts.reportLoadError(moduleHandle); return null; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/DamageTint.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/DamageTint.java index e176122..236092b 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/DamageTint.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/DamageTint.java @@ -38,7 +38,6 @@ public class DamageTint extends Module implements PostProcessPass { private static final String SETTING_PULSE_MAX = "pulse_max"; private static final String SETTING_FADE_IN = "fade_in"; private static final String SETTING_FADE_OUT = "fade_out"; - private final Minecraft mc = Minecraft.getInstance(); private final NumberValue<Integer> startHearts = num("damageTintStartHearts", SETTING_START_HP, 20, 1, 80); private final NumberValue<Integer> fullHearts = @@ -71,6 +70,7 @@ public class DamageTint extends Module implements PostProcessPass { @Override public boolean isActive() { + Minecraft mc = Minecraft.getInstance(); return isEnabled() && mc.player != null && mc.level != null; } @@ -86,6 +86,7 @@ public class DamageTint extends Module implements PostProcessPass { @Override public boolean render(GpuTextureView src, GpuTextureView dst, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.player == null || mc.level == null) return false; if (src == null || dst == null) return false; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/Predictions.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/Predictions.java index 1afa626..8ffbb64 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/Predictions.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/Predictions.java @@ -133,8 +133,6 @@ public class Predictions extends Module { private static final float IMPACT_RING_LINE_WIDTH = 1.2f; private static final float IMPACT_RING_ALPHA = 0.88f; - private final Minecraft mc = Minecraft.getInstance(); - private final RGBColorValue lineColorValue = colorNoAlpha("pred_line_color", SETTING_LINE_COLOR, "#11C7C7"); private final BooleanValue pearlColorToggle = bool("pred_pearl_custom_color", SETTING_PEARL_CUSTOM_COLOR, false); private final RGBColorValue pearlColorValue = @@ -191,6 +189,7 @@ public class Predictions extends Module { @Override public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.level == null) { TargetManager.setPredictionTarget(null); return; @@ -215,6 +214,7 @@ public class Predictions extends Module { @Override public void onRenderHudEngine(Renderer2D renderer, TextRenderer textRenderer, net.minecraft.client.gui.GuiGraphicsExtractor ctx, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.level == null) return; boolean showPlate = showTimerPlateValue.get(); @@ -238,6 +238,7 @@ public class Predictions extends Module { @Override public void onRenderHudEngineForeground(Renderer2D renderer, TextRenderer textRenderer, net.minecraft.client.gui.GuiGraphicsExtractor ctx, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.level == null) return; if (timerPlates.isEmpty()) return; @@ -276,6 +277,7 @@ public class Predictions extends Module { } private List<TrajectoryResult> simulateProjectiles(float tickDelta) { + Minecraft mc = Minecraft.getInstance(); ClientLevel world = mc.level; if (world == null) return Collections.emptyList(); @@ -396,6 +398,7 @@ public class Predictions extends Module { } private void drawTntTimers(Renderer2D renderer, TextRenderer textRenderer, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); ClientLevel world = mc.level; if (world == null) return; @@ -518,6 +521,7 @@ public class Predictions extends Module { } private void renderAimTrajectoryGlow(Renderer3D renderer, float tickDelta, int baseColor) { + Minecraft mc = Minecraft.getInstance(); Player player = mc.player; if (player == null) return; @@ -574,6 +578,7 @@ public class Predictions extends Module { } private void renderAimIndicatorWorld(Renderer3D renderer, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); Player player = mc.player; if (player == null) return; @@ -600,6 +605,7 @@ public class Predictions extends Module { } private void updatePredictionTarget(float tickDelta) { + Minecraft mc = Minecraft.getInstance(); if (!targetHudOnAimValue.get()) { TargetManager.setPredictionTarget(null); return; @@ -710,6 +716,7 @@ public class Predictions extends Module { } private void renderImpactEffects(Renderer3D renderer, List<TrajectoryResult> results, int baseColor, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); boolean walls = glowWallsValue.get(); RenderPipeline linePipeline = walls ? LoVisualRenderPipelines.WORLD_COLORED_LINES_LIQUID_BLEND diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/ProjectilePuncher.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/ProjectilePuncher.java index 180610f..b27cdbf 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/ProjectilePuncher.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/ProjectilePuncher.java @@ -78,7 +78,6 @@ public final class ProjectilePuncher extends Module { private static final String PROJECTILE_FIREBALL = "fireball"; private static final String PROJECTILE_SHULKER_BULLET = "shulker_bullet"; private static final int ROTATION_PRIORITY = 10; - private final Minecraft mc = Minecraft.getInstance(); private final NumberValue<Double> range = numCommon( "projectilePuncherRange", @@ -428,6 +427,7 @@ public final class ProjectilePuncher extends Module { @Override public void onTick() { + Minecraft mc = Minecraft.getInstance(); if (mc.player == null || mc.level == null) { clearTargetImmediate(); } @@ -435,6 +435,7 @@ public final class ProjectilePuncher extends Module { @EventHandler(priority = ROTATION_PRIORITY) public void onRotationUpdate(RotationUpdateEvent event) { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled()) return; if (mc.player == null || mc.level == null) { clearTargetImmediate(); @@ -461,6 +462,7 @@ public final class ProjectilePuncher extends Module { } private void tryAttackPost() { + Minecraft mc = Minecraft.getInstance(); if (mc.player == null || mc.level == null || mc.gameMode == null) return; if (mc.player.tickCount == lastAttackExecutionAge) { return; @@ -493,6 +495,7 @@ public final class ProjectilePuncher extends Module { } private List<Entity> findCandidates() { + Minecraft mc = Minecraft.getInstance(); double searchRange = Math.max(range.get(), wallRange.get()) + 8.0; List<Entity> candidates = mc.level.getEntities( mc.player, @@ -506,6 +509,7 @@ public final class ProjectilePuncher extends Module { } private boolean processTarget(Entity target, float range, float wallsRange) { + Minecraft mc = Minecraft.getInstance(); RotationWithVector rotation = findRotation(target, range, wallsRange); if (rotation == null) return false; @@ -526,6 +530,7 @@ public final class ProjectilePuncher extends Module { } private Rotation resolveAttackRotation(Entity target) { + Minecraft mc = Minecraft.getInstance(); if (mc.player == null) return null; Rotation rotation; @@ -548,6 +553,7 @@ public final class ProjectilePuncher extends Module { } private boolean passesRaycast(Entity target, float yaw, float pitch) { + Minecraft mc = Minecraft.getInstance(); RaycastMode mode = raycast.get(); if (mode == RaycastMode.NONE) return true; @@ -560,10 +566,12 @@ public final class ProjectilePuncher extends Module { } private boolean isInRange(Entity target) { + Minecraft mc = Minecraft.getInstance(); return TargetingUtil.distanceToEntityBoxSq(mc.player.getEyePosition(), target) <= range.get() * range.get(); } private RotationWithVector findRotation(Entity target, float range, float wallsRange) { + Minecraft mc = Minecraft.getInstance(); if (mc.player == null) return null; Vec3 eyes = mc.player.getEyePosition(); @@ -635,10 +643,12 @@ public final class ProjectilePuncher extends Module { } private double predictedDistanceSq(Entity entity) { + Minecraft mc = Minecraft.getInstance(); return TargetingUtil.distanceToBoxSq(mc.player.getEyePosition(), predictBox(entity)); } private boolean shouldAttack(Entity entity) { + Minecraft mc = Minecraft.getInstance(); Vec3 velocity = entity.getDeltaMovement(); if (velocity.lengthSqr() < 1.0E-7) { return false; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/RubberHand.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/RubberHand.java index fd30114..69fa873 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/RubberHand.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/RubberHand.java @@ -37,7 +37,6 @@ public class RubberHand extends Module { private static final String ACTION_ATTACK_WHILE_SHIELD = "attack_while_shield"; private static final String ACTION_ATTACK_WHILE_EATING = "attack_while_eating"; - private final Minecraft mc = Minecraft.getInstance(); private final BooleanMapValue actions = group( "rubberhand_actions", SETTING_ACTIONS, @@ -66,6 +65,7 @@ public class RubberHand extends Module { @Override public void onTick() { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled()) return; if (mc.player == null) return; @@ -78,6 +78,7 @@ public class RubberHand extends Module { @EventHandler private void onSync(EventSync event) { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.player == null || mc.gameMode == null) return; if (didAttackThisHold) return; if (!mc.options.keyAttack.isDown() || !mc.options.keyUse.isDown()) return; @@ -119,6 +120,7 @@ public class RubberHand extends Module { } private void performAttack(LocalPlayer player, Entity target) { + Minecraft mc = Minecraft.getInstance(); mc.gameMode.attack(player, target); player.swing(InteractionHand.MAIN_HAND); didAttackThisHold = true; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/TPSSync.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/TPSSync.java index 01c8b59..b41cff6 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/TPSSync.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/TPSSync.java @@ -24,7 +24,6 @@ import dev.loki.lovisual.util.player.NetworkStatsUtil; public class TPSSync extends Module { private static final String SETTING_START_TPS = "start_tps"; - private final Minecraft mc = Minecraft.getInstance(); private final BooleanValue enabled = new BooleanValue("tpssync_enabled", true); private final NumberValue<Float> startTps = @@ -32,6 +31,7 @@ public class TPSSync extends Module { /** сколько серверных тиков прошло за клиентский */ public float getServerTickDelta() { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || !enabled.get()) return 1.0f; if (mc == null || mc.level == null) return 1.0f; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/TargetESP.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/TargetESP.java index 7202813..b1c3388 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/TargetESP.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/combat/TargetESP.java @@ -78,7 +78,6 @@ public class TargetESP extends Module { private static final Identifier CRYSTAL_BLOOM = TextureStorage.BLOOM; private static final float ALPHA_TICK_DT = 1.0f / 20.0f; private static final float ALPHA_SNAP_EPSILON = 0.02f; - private final Minecraft mc = Minecraft.getInstance(); private final ModeValue espMode = modeSetting( "targetEspMode", SETTING_MODE, @@ -146,6 +145,7 @@ public class TargetESP extends Module { @Override public void onTick() { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.level == null) { target = null; updateRenderTarget(null); @@ -247,6 +247,7 @@ public class TargetESP extends Module { } private Renderer3D.CullOptions resolveCullOptions(LivingEntity target) { + Minecraft mc = Minecraft.getInstance(); if (target == null || mc.player == null) { return Renderer3D.CullOptions.SMART_TARGET; } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/DefineTarget.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/DefineTarget.java index 457c5dd..60f7555 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/DefineTarget.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/DefineTarget.java @@ -48,7 +48,6 @@ public class DefineTarget extends Module { private static final String SETTING_BLOCK_DAMAGE_TO_BEDWARS = "block_damage_to_bedwars"; private static final String SETTING_IGNORED_ENTITIES = "ignored_entities"; private static final String ACTION_TOGGLE_TARGET = "toggle_target"; - private final Minecraft mc = Minecraft.getInstance(); private final BooleanValue bedwarsRelations = bool("defineTargetBedwarsRelations", SETTING_BEDWARS_RELATIONS, false); private final EnumValue<BedwarsColorMode> bedwarsColorMode = visibleWhen(enumSetting("defineTargetBedwarsColorMode", SETTING_BEDWARS_COLOR_MODE, BedwarsColorMode.RELATION_COLORS, BedwarsColorMode.values()), @@ -142,6 +141,7 @@ public class DefineTarget extends Module { @Override public void onTick() { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.player == null || mc.level == null) return; if (isActionPressedOnce(ACTION_TOGGLE_TARGET)) { @@ -150,6 +150,7 @@ public class DefineTarget extends Module { } private void handleToggle() { + Minecraft mc = Minecraft.getInstance(); HitResult hit = mc.hitResult; if (!(hit instanceof EntityHitResult ehr)) return; Entity ent = ehr.getEntity(); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/FakePlayer.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/FakePlayer.java index 7561f0d..2842a7b 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/FakePlayer.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/FakePlayer.java @@ -55,7 +55,6 @@ public class FakePlayer extends Module { private static final String SETTING_AUTO_TOTEM = "auto_totem"; private static final float SMASH_THRESHOLD = 1.5f; private static final float SMASH_BASE_DAMAGE_PER_BLOCK = 1.0f; - private final Minecraft mc = Minecraft.getInstance(); private final BooleanValue copyInventory = bool("fakePlayerCopyInventory", SETTING_COPY_INVENTORY, false); private final BooleanValue autoTotem = @@ -65,6 +64,7 @@ public class FakePlayer extends Module { private int deathTime; public FakePlayer() { + Minecraft mc = Minecraft.getInstance(); AttackEntityCallback.EVENT.register((player, world, hand, target, hitResult) -> { if (!isEnabled()) return InteractionResult.PASS; if (mc.player == null || mc.level == null) return InteractionResult.PASS; @@ -112,6 +112,7 @@ public class FakePlayer extends Module { @Override public void onEnable() { + Minecraft mc = Minecraft.getInstance(); if (mc.level == null || mc.player == null) return; despawnFake(); @@ -168,6 +169,7 @@ public class FakePlayer extends Module { @Override public void onTick() { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.level == null || mc.player == null) return; if (fakePlayer == null) return; @@ -186,6 +188,7 @@ public class FakePlayer extends Module { } private int allocateFakeEntityId() { + Minecraft mc = Minecraft.getInstance(); int id = nextFakeEntityId--; while (mc.level != null && mc.level.getEntity(id) != null) { id = nextFakeEntityId--; @@ -204,6 +207,7 @@ public class FakePlayer extends Module { } private void handleAttack(Player attacker, Player target) { + Minecraft mc = Minecraft.getInstance(); if (mc.level == null) return; ItemStack weapon = attacker.getMainHandItem(); @@ -270,6 +274,7 @@ public class FakePlayer extends Module { } private void applyDamage(Player target, DamageSource source, float damage) { + Minecraft mc = Minecraft.getInstance(); if (damage <= 0f) return; target.handleDamageEvent(source); @@ -335,6 +340,7 @@ public class FakePlayer extends Module { } private Holder<Enchantment> entryOf(ResourceKey<Enchantment> key) { + Minecraft mc = Minecraft.getInstance(); if (key == null || mc.level == null) return null; Registry<Enchantment> registry = mc.level.registryAccess().lookupOrThrow(Registries.ENCHANTMENT); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/ItemScroller.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/ItemScroller.java new file mode 100644 index 0000000..5d21028 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/ItemScroller.java @@ -0,0 +1,68 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.misc; + +import dev.loki.lovisual.config.values.BooleanValue; +import dev.loki.lovisual.features.module.Module; +import dev.loki.lovisual.features.module.ModuleCategory; +import dev.loki.lovisual.features.module.ModuleInfo; +import dev.loki.lovisual.util.screen.ClientScreen; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerInput; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; + +/** + * Shift-moves items with the mouse wheel: scrolling over a container screen + * sends a QUICK_MOVE click for the hovered slot, or for the matching hotbar + * slot when the cursor is over no slot at all. + */ +@ModuleInfo( + id = "itemscroller", + displayName = "Item Scroller", + aliases = {"itemscroller", "scrollmove"}, + category = ModuleCategory.MISC +) +public class ItemScroller extends Module { + private static final String SETTING_HOTBAR_FALLBACK = "hotbar_fallback"; + + private final BooleanValue hotbarFallback = + bool("itemScrollerHotbarFallback", SETTING_HOTBAR_FALLBACK, true); + + /** Called from AbstractContainerScreenMixin before vanilla scroll handling. */ + public boolean onMouseScrolled(double vertical, Object hoveredSlotObj, ItemStack hoveredStack) { + if (!isEnabled() || !ScrollMovePolicy.isScrollSignificant(vertical)) return false; + + Minecraft mc = Minecraft.getInstance(); + if (mc.player == null || mc.gameMode == null) return false; + if (!(ClientScreen.current() instanceof AbstractContainerScreen<?> screen)) return false; + + AbstractContainerMenu menu = screen.getMenu(); + net.minecraft.world.inventory.Slot hovered = (net.minecraft.world.inventory.Slot) hoveredSlotObj; + int target = hovered != null + ? ScrollMovePolicy.resolveTargetSlot(true, hovered.index, -1, menu.slots.size()) + : hotbarFallbackTarget(menu, mc.player); + + if (!ScrollMovePolicy.canClick(menu, target, hoveredStack)) return false; + if (hovered == null && menu.getCarried() != null && !menu.getCarried().isEmpty()) { + // Holding an item stack: let vanilla handle the drag. + return false; + } + + mc.gameMode.handleContainerInput(menu.containerId, target, + ScrollMovePolicy.quickMoveButton(), ContainerInput.QUICK_MOVE, mc.player); + return true; + } + + private int hotbarFallbackTarget(AbstractContainerMenu menu, Player player) { + if (!hotbarFallback.get()) return -1; + return ScrollMovePolicy.resolveTargetSlot(false, -1, + player.getInventory().getSelectedSlot(), menu.slots.size()); + }} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/NameProtect.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/NameProtect.java new file mode 100644 index 0000000..01a4da2 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/NameProtect.java @@ -0,0 +1,116 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.misc; + +import dev.loki.lovisual.config.values.BooleanValue; +import dev.loki.lovisual.config.values.StringValue; +import dev.loki.lovisual.features.module.Module; +import dev.loki.lovisual.features.module.ModuleCategory; +import dev.loki.lovisual.features.module.ModuleInfo; +import dev.loki.lovisual.features.module.Modules; +import dev.loki.lovisual.features.relations.PlayerRelations; +import dev.loki.lovisual.util.text.NameProtector; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.ComponentContents; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.chat.contents.PlainTextContents; + +/** + * Hides the local nickname and friend nicknames in rendered chat/tab text, + * replacing them with a configurable alias. Hook points are the mixins on + * ChatComponent.addMessage and PlayerInfo.getTabListDisplayName. + */ +@ModuleInfo( + id = "nameprotect", + displayName = "Name Protect", + category = ModuleCategory.MISC +) +public class NameProtect extends Module { + + private static final String SETTING_ALIAS = "alias"; + private static final String SETTING_PROTECT_SELF = "protect_self"; + private static final String SETTING_PROTECT_FRIENDS = "protect_friends"; + private static final long CACHE_TTL_MS = 100L; + + private final StringValue alias = text("nameProtectAlias", SETTING_ALIAS, "Protected"); + private final BooleanValue protectSelf = bool("nameProtectSelf", SETTING_PROTECT_SELF, true); + private final BooleanValue protectFriends = bool("nameProtectFriends", SETTING_PROTECT_FRIENDS, true); + + private volatile NameProtector cachedProtector; + private volatile long cachedAt; + + @Override + public void onDisable() { + cachedProtector = null; + } + + /** + * @return null when the module is off or nothing is configured to protect + */ + public static NameProtector activeProtector() { + NameProtect module = Modules.get(NameProtect.class); + if (module == null || !module.isEnabled()) return null; + return module.protector(); + } + + /** Rewrites plain-text leaves of a component tree; returns input when nothing changes. */ + public static Component protect(Component text) { + if (text == null) return null; + NameProtector protector = activeProtector(); + if (protector == null) return text; + return rewrite(text, protector); + } + + private NameProtector protector() { + long now = System.currentTimeMillis(); + NameProtector cached = cachedProtector; + if (cached != null && now - cachedAt < CACHE_TTL_MS) { + return cached.isEmpty() ? null : cached; + } + + NameProtector protector = build(); + cachedProtector = protector; + cachedAt = now; + return protector.isEmpty() ? null : protector; + } + + private NameProtector build() { + NameProtector protector = new NameProtector(); + String replacement = alias.get(); + if (replacement == null) replacement = ""; + Minecraft mc = Minecraft.getInstance(); + + if (protectSelf.get() && mc != null && mc.getUser() != null) { + protector.put(mc.getUser().getName(), replacement); + } + if (protectFriends.get()) { + for (String friend : PlayerRelations.get().getFriends()) { + protector.put(friend, replacement); + } + } + return protector; + } + + private static Component rewrite(Component text, NameProtector protector) { + ComponentContents contents = text.getContents(); + MutableComponent result; + + if (contents instanceof PlainTextContents plain) { + String replaced = protector.replace(plain.text()); + result = Component.literal(replaced).withStyle(text.getStyle()); + } else { + result = MutableComponent.create(contents).withStyle(text.getStyle()); + } + + for (Component child : text.getSiblings()) { + result.append(rewrite(child, protector)); + } + return result; + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/NoSound.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/NoSound.java index 391ba85..991d00b 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/NoSound.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/NoSound.java @@ -7,6 +7,8 @@ package dev.loki.lovisual.features.module.modules.misc; +import dev.loki.lovisual.config.values.ModeValue; +import dev.loki.lovisual.config.values.NumberValue; import dev.loki.lovisual.config.values.SetValue; import dev.loki.lovisual.features.gui.clickgui.settings.TextListSetting; import dev.loki.lovisual.features.module.Module; @@ -24,14 +26,45 @@ import java.util.Set; ) public class NoSound extends Module { private static final String SETTING_SOUND_IDS = "sound_ids"; + private static final String SETTING_MODE = "mode"; + private static final String SETTING_VOLUME_SCALE = "volume_scale"; + private final SetValue soundIds = textList("noSoundIds", SETTING_SOUND_IDS, TextListSetting.PickerMode.SOUNDS); + private final ModeValue mode = mode("noSoundMode", SETTING_MODE, + Mode.MUTE.id, Mode.REDUCE.id); + private final NumberValue<Integer> volumeScale = + visibleWhen(num("noSoundVolumeScale", SETTING_VOLUME_SCALE, 30, 1, 100), + () -> Mode.REDUCE.id.equals(mode.get())); { soundIds.set(Set.of("minecraft:entity.ender_dragon.growl")); } + public enum Mode { + MUTE("Mute"), + REDUCE("Reduce"); + + public final String id; + + Mode(String id) { + this.id = id; + } + } + public boolean shouldMute(String id) { - if (!isEnabled()) return false; + return isEnabled() && Mode.MUTE.id.equals(mode.get()) && matches(id); + } + + /** + * Volume multiplier for the sound engine: 1.0 unless the sound is listed + * and the module is in reduce mode. + */ + public float volumeScale(String id) { + if (!isEnabled() || !Mode.REDUCE.id.equals(mode.get())) return 1.0f; + return resolveScale(matches(id), volumeScale.get()); + } + + private boolean matches(String id) { if (id == null || id.isEmpty()) return false; String normalized = id.toLowerCase(Locale.ROOT); for (String mutedId : soundIds.get()) { @@ -39,4 +72,10 @@ public class NoSound extends Module { } return false; } + + /** Pure scale math shared with the sound-engine mixin. */ + public static float resolveScale(boolean matches, int scalePct) { + if (!matches) return 1.0f; + return Math.max(0, Math.min(100, scalePct)) / 100.0f; + } } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/ScrollMovePolicy.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/ScrollMovePolicy.java new file mode 100644 index 0000000..d490fdc --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/ScrollMovePolicy.java @@ -0,0 +1,67 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.misc; + +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.item.ItemStack; + +/** + * Pure scroll-quick-move logic for ItemScroller: resolves which slot should + * receive a QUICK_MOVE click when the user scrolls over the screen. + */ +public final class ScrollMovePolicy { + + private ScrollMovePolicy() { + } + + public static boolean isScrollSignificant(double vertical) { + return vertical > 0.05 || vertical < -0.05; + } + + public static int direction(double vertical) { + return vertical > 0 ? 1 : -1; + } + + /** + * QUICK_MOVE button (shift-click) as understood by + * {@code MultiPlayerGameMode.handleContainerInput} for a left click. + */ + public static int quickMoveButton() { + return 0; + } + + /** + * Resolves the slot index that should receive the shift-click: the hovered + * slot when it exists, otherwise the hotbar slot matching the scroll + * direction (vanilla hotbar cycling) mapped into the menu. + */ + public static int resolveTargetSlot(boolean hasHovered, int hoveredIndex, + int hotbarSlot, int menuSlotCount) { + if (hasHovered) return hoveredIndex; + return hotbarIndexInMenu(hotbarSlot, menuSlotCount); + } + + public static int hotbarIndexInMenu(int hotbarSlot, int menuSlotCount) { + if (menuSlotCount <= 0) return -1; + // In the standard inventory menu the hotbar occupies the last 9 slots. + int hotbarStart = menuSlotCount - Inventory.getSelectionSize(); + if (hotbarStart < 0) return -1; + return hotbarStart + Math.floorMod(hotbarSlot, Inventory.getSelectionSize()); + } + + /** + * Double-check before sending a packet: an empty source stack must never + * produce a click, and the slot must belong to the open menu. + */ + public static boolean canClick(AbstractContainerMenu menu, int slotIndex, ItemStack hoveredStack) { + if (menu == null || slotIndex < 0 || slotIndex >= menu.slots.size()) return false; + if (hoveredStack == null || hoveredStack.isEmpty()) return false; + return true; + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/StreamerMode.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/StreamerMode.java new file mode 100644 index 0000000..101351c --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/misc/StreamerMode.java @@ -0,0 +1,121 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.misc; + +import dev.loki.lovisual.config.values.BooleanValue; +import dev.loki.lovisual.config.values.StringValue; +import dev.loki.lovisual.features.module.Module; +import dev.loki.lovisual.features.module.ModuleCategory; +import dev.loki.lovisual.features.module.ModuleInfo; +import dev.loki.lovisual.features.module.Modules; +import dev.loki.lovisual.util.text.NameMasker; + +import java.util.List; + +/** + * Streamer mode: masks every nickname-like token in chat/tab/scoreboard text + * (so both player names and server-brand words disappear deterministically), + * and can replace real player skins with the default Steve/Alex texture. + * Hook points reuse the NameProtect mixins plus a skin override in + * AbstractClientPlayerMixin. + */ +@ModuleInfo( + id = "streamermode", + displayName = "Streamer Mode", + aliases = {"streamermode", "streamer"}, + category = ModuleCategory.MISC +) +public class StreamerMode extends Module { + private static final String SETTING_ALIAS = "alias"; + private static final String SETTING_MASK_CHAT = "mask_chat"; + private static final String SETTING_MASK_SERVER_WORDS = "mask_server_words"; + private static final String SETTING_HIDE_SKINS = "hide_skins"; + + private final StringValue alias = text("streamerModeAlias", SETTING_ALIAS, "Player"); + private final BooleanValue maskChat = bool("streamerModeMaskChat", SETTING_MASK_CHAT, true); + private final BooleanValue maskServerWords = + visibleWhen(bool("streamerModeMaskServerWords", SETTING_MASK_SERVER_WORDS, true), + maskChat::get); + private final BooleanValue hideSkins = bool("streamerModeHideSkins", SETTING_HIDE_SKINS, true); + + private volatile NameMasker cachedMasker; + + @Override + public void onDisable() { + cachedMasker = null; + } + + public boolean shouldHideSkins() { + return isEnabled() && hideSkins.get(); + } + + public boolean shouldMaskChat() { + return isEnabled() && maskChat.get(); + } + + /** @return null when the module is off or chat masking is disabled */ + public static NameMasker activeMasker() { + StreamerMode module = Modules.get(StreamerMode.class); + if (module == null || !module.shouldMaskChat()) return null; + return module.masker(); + } + + /** Rewrites chat/tab component text; mirrors NameProtect.protect usage. */ + public static String maskText(String text) { + if (text == null) return null; + NameMasker masker = activeMasker(); + if (masker == null) return text; + return masker.mask(text); + } + + /** + * Component-tree variant for chat mixins: rewrites plain-text leaves and + * returns the input instance when nothing changed. + */ + public static net.minecraft.network.chat.Component protectComponent(net.minecraft.network.chat.Component text) { + if (text == null) return null; + NameMasker masker = activeMasker(); + if (masker == null) return text; + + net.minecraft.network.chat.ComponentContents contents = text.getContents(); + net.minecraft.network.chat.MutableComponent result; + if (contents instanceof net.minecraft.network.chat.contents.PlainTextContents plain) { + String replaced = masker.mask(plain.text()); + if (replaced.equals(plain.text())) { + result = net.minecraft.network.chat.MutableComponent.create(contents).withStyle(text.getStyle()); + } else { + result = net.minecraft.network.chat.Component.literal(replaced).withStyle(text.getStyle()); + } + } else { + result = net.minecraft.network.chat.MutableComponent.create(contents).withStyle(text.getStyle()); + } + + for (net.minecraft.network.chat.Component child : text.getSiblings()) { + result.append(protectComponent(child)); + } + return result; + } + + private NameMasker masker() { + NameMasker cached = cachedMasker; + if (cached != null) return cached; + + List<String> words = maskServerWords.get() + ? List.of("anarchy", "анархия", "hitech", "хайтек", "magic", "магия", + "vanilla", "ванил", "skyblock", "скайблок") + : List.of(); + NameMasker masker = new NameMasker(alias.get(), words); + cachedMasker = masker; + return masker; + } + + /** Pure helper for the skin mixin: is this UUID worth hiding (not self). */ + public static boolean hideSkin(boolean enabled, boolean isSelf) { + return enabled && !isSelf; + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/AutoAccept.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/AutoAccept.java new file mode 100644 index 0000000..965bb94 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/AutoAccept.java @@ -0,0 +1,140 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.player; + +import dev.loki.lovisual.config.values.BooleanValue; +import dev.loki.lovisual.config.values.NumberValue; +import dev.loki.lovisual.config.values.SetValue; +import dev.loki.lovisual.config.values.StringValue; +import dev.loki.lovisual.events.EventHandler; +import dev.loki.lovisual.events.impl.PvpChatEvent; +import dev.loki.lovisual.features.gui.clickgui.settings.TextListSetting; +import dev.loki.lovisual.features.module.Module; +import dev.loki.lovisual.features.module.ModuleCategory; +import dev.loki.lovisual.features.module.ModuleInfo; +import dev.loki.lovisual.features.module.Notifier; +import dev.loki.lovisual.features.relations.PlayerRelations; +import dev.loki.lovisual.util.time.GameClock; +import net.minecraft.client.Minecraft; + +import java.util.Locale; +import java.util.Set; + +/** + * Answers teleport/clan requests automatically by sending a configurable + * command once a chat message matches one of the configured patterns. + * Optionally restricted to friends via PlayerRelations. + */ +@ModuleInfo( + id = "autoaccept", + displayName = "Auto Accept", + aliases = {"autoaccept"}, + category = ModuleCategory.PLAYER +) +public class AutoAccept extends Module { + private static final String SETTING_PATTERNS = "patterns"; + private static final String SETTING_COMMAND = "command"; + private static final String SETTING_ONLY_FRIENDS = "only_friends"; + private static final String SETTING_DELAY = "delay"; + private static final String SETTING_COOLDOWN = "cooldown"; + + private static final long MIN_COOLDOWN_MS = 500L; + + private final SetValue patterns = textList("autoAcceptPatterns", SETTING_PATTERNS, + TextListSetting.PickerMode.TEXT, + Set.of( + "просит телепортироваться к себе", + "просит телепортироваться", + "хочет телепортироваться к вам", + "has requested to teleport to you", + "has requested to teleport", + "wants to teleport to you" + )); + private final StringValue command = text("autoAcceptCommand", SETTING_COMMAND, "tpaccept"); + private final BooleanValue onlyFriends = bool("autoAcceptOnlyFriends", SETTING_ONLY_FRIENDS, false); + private final NumberValue<Integer> delay = num("autoAcceptDelay", SETTING_DELAY, 0, 0, 3000); + private final NumberValue<Integer> cooldown = + num("autoAcceptCooldown", SETTING_COOLDOWN, 2000, (int) MIN_COOLDOWN_MS, 10000); + + private String pendingSender; + private long firstMatchMs; + private long lastAcceptedMs = Long.MIN_VALUE; + + @Override + public void onDisable() { + pendingSender = null; + } + + @Override + public void onTick() { + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.player == null || mc.getConnection() == null) { + pendingSender = null; + return; + } + if (pendingSender == null) return; + + long now = GameClock.millis(); + if (now - firstMatchMs < delay.get()) return; + String sender = pendingSender; + pendingSender = null; + + if (now - lastAcceptedMs < Math.max(MIN_COOLDOWN_MS, cooldown.get())) return; + + lastAcceptedMs = now; + Notifier.info("Auto Accept: " + sender); + mc.getConnection().sendCommand(command.get()); + } + + @EventHandler + private void onChat(PvpChatEvent event) { + if (event.message == null || event.message.isBlank()) return; + + String sender = TeleportRequestParser.extractSender(event.message, patterns.get()); + if (sender == null) return; + if (onlyFriends.get() && !PlayerRelations.get().isFriend(sender)) return; + if (pendingSender != null) return; + + pendingSender = sender; + firstMatchMs = GameClock.millis(); + } + + /** + * Pure chat-request parsing: finds the earliest configured pattern and + * extracts the nickname-like token immediately before it. + */ + public static final class TeleportRequestParser { + private TeleportRequestParser() { + } + + public static String extractSender(String message, Set<String> patterns) { + if (message == null || patterns == null || patterns.isEmpty()) return null; + + String lower = message.toLowerCase(Locale.ROOT); + int matchIndex = -1; + for (String pattern : patterns) { + if (pattern == null || pattern.isBlank()) continue; + int idx = lower.indexOf(pattern.toLowerCase(Locale.ROOT)); + if (idx >= 0 && (matchIndex < 0 || idx < matchIndex)) matchIndex = idx; + } + if (matchIndex <= 0) return null; + + return lastNicknameIn(message.substring(0, matchIndex)); + } + + static String lastNicknameIn(String prefix) { + java.util.regex.Matcher matcher = + java.util.regex.Pattern.compile("[A-Za-z0-9_]{3,16}").matcher(prefix); + String found = null; + while (matcher.find()) { + found = matcher.group(); + } + return found; + } + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/AutoLeave.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/AutoLeave.java new file mode 100644 index 0000000..2850756 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/AutoLeave.java @@ -0,0 +1,159 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.player; + +import dev.loki.lovisual.config.values.BooleanValue; +import dev.loki.lovisual.config.values.NumberValue; +import dev.loki.lovisual.features.module.Module; +import dev.loki.lovisual.features.module.ModuleCategory; +import dev.loki.lovisual.features.module.ModuleInfo; +import dev.loki.lovisual.features.module.Notifier; +import dev.loki.lovisual.features.relations.PlayerRelations; +import dev.loki.lovisual.util.time.GameClock; +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.world.entity.player.Player; + +/** + * Leaves the server by simple safety triggers: low health or unfriendly + * players nearby. Fires once, after a configurable delay, with a warning + * notification so the leave never comes as a surprise. + */ +@ModuleInfo( + id = "autoleave", + displayName = "Auto Leave", + aliases = {"autoleave", "autodc"}, + category = ModuleCategory.PLAYER +) +public class AutoLeave extends Module { + private static final String SETTING_HP_ENABLED = "hp_enabled"; + private static final String SETTING_HP_THRESHOLD = "hp_threshold"; + private static final String SETTING_PLAYERS_ENABLED = "players_enabled"; + private static final String SETTING_PLAYER_COUNT = "player_count"; + private static final String SETTING_RADIUS = "radius"; + private static final String SETTING_IGNORE_FRIENDS = "ignore_friends"; + private static final String SETTING_DELAY = "delay"; + + private final BooleanValue hpEnabled = bool("autoLeaveHpEnabled", SETTING_HP_ENABLED, true); + private final NumberValue<Double> hpThreshold = + visibleWhen(num("autoLeaveHpThreshold", SETTING_HP_THRESHOLD, 6.0, 1.0, 19.0), hpEnabled::get); + private final BooleanValue playersEnabled = bool("autoLeavePlayersEnabled", SETTING_PLAYERS_ENABLED, false); + private final NumberValue<Integer> playerCount = + visibleWhen(num("autoLeavePlayerCount", SETTING_PLAYER_COUNT, 1, 1, 20), playersEnabled::get); + private final NumberValue<Integer> radius = + visibleWhen(num("autoLeaveRadius", SETTING_RADIUS, 32, 8, 128), playersEnabled::get); + private final BooleanValue ignoreFriends = + visibleWhen(bool("autoLeaveIgnoreFriends", SETTING_IGNORE_FRIENDS, true), playersEnabled::get); + private final NumberValue<Integer> delay = num("autoLeaveDelay", SETTING_DELAY, 1000, 0, 5000); + + private final LeavePolicy policy = new LeavePolicy(); + private LocalPlayer lastPlayer; + + @Override + public void onDisable() { + lastPlayer = null; + policy.reset(); + } + + @Override + public void onFrame(float tickDelta) { + // onTick only fires while a player exists; re-arming on world change + // has to happen where the player absence is still observable. + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.player == null) { + lastPlayer = null; + policy.reset(); + return; + } + if (mc.player != lastPlayer) { + lastPlayer = mc.player; + policy.reset(); + } + } + + @Override + public void onTick() { + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.player == null || mc.level == null) return; + + boolean triggered = isLowHealth(mc.player) || hasUnfriendlyPlayersNearby(mc); + if (!policy.onTick(triggered, GameClock.millis(), delay.get())) return; + if (policy.consumeWarning()) { + Notifier.warning("Auto Leave: disconnecting now"); + } + leave(mc); + } + + private boolean isLowHealth(LocalPlayer player) { + return hpEnabled.get() && LeavePolicy.isHealthLow(player.getHealth(), hpThreshold.get().floatValue()); + } + + private boolean hasUnfriendlyPlayersNearby(Minecraft mc) { + if (!playersEnabled.get()) return false; + + double radiusSq = (double) radius.get() * radius.get(); + int unfriendly = 0; + for (Player other : mc.level.players()) { + if (other == mc.player || other.isSpectator()) continue; + if (!other.isAlive()) continue; + if (ignoreFriends.get() && PlayerRelations.get().isFriend(other.getGameProfile().name())) continue; + if (other.distanceToSqr(mc.player) > radiusSq) continue; + unfriendly++; + } + return LeavePolicy.isPlayerCountReached(unfriendly, playerCount.get()); + } + + private void leave(Minecraft mc) { + policy.reset(); + mc.disconnect(null, false); + } + + /** + * Pure trigger state machine: warn once on a rising trigger edge, then + * leave after the configured delay. Any quiet tick re-arms the gate. + */ + public static final class LeavePolicy { + private boolean triggered; + private long triggerMs; + private boolean warningShown; + + public boolean onTick(boolean anyTrigger, long nowMs, long delayMs) { + if (!anyTrigger) { + reset(); + return false; + } + if (!triggered) { + triggered = true; + triggerMs = nowMs; + warningShown = false; + return Math.max(0L, delayMs) <= 0L; + } + return nowMs - triggerMs >= Math.max(0L, delayMs); + } + + public boolean consumeWarning() { + if (warningShown) return false; + warningShown = true; + return true; + } + + public void reset() { + triggered = false; + triggerMs = 0L; + warningShown = false; + } + + public static boolean isHealthLow(float health, float threshold) { + return health <= threshold; + } + + public static boolean isPlayerCountReached(int count, int required) { + return count >= Math.max(1, required); + } + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/AutoTool.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/AutoTool.java index 2b89b54..e4693ea 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/AutoTool.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/AutoTool.java @@ -35,13 +35,12 @@ public class AutoTool extends Module { private final NumberValue<Integer> restoreDelayMs = num("autoToolRestoreDelayMs", SETTING_RESTORE_DELAY, 300, 0, 1000); - private final Minecraft mc = Minecraft.getInstance(); - private int originalSlot = -1; private long lastBreakMs = 0L; @Override public void onDisable() { + Minecraft mc = Minecraft.getInstance(); if (mc != null && mc.player != null && originalSlot >= 0) { InventorySwap.INSTANCE.selectHotbar(originalSlot); } @@ -51,6 +50,7 @@ public class AutoTool extends Module { @EventHandler private void onGameTick(GameTickEvent event) { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.player == null || mc.level == null || mc.options == null || mc.gameMode == null) { resetState(); return; @@ -83,6 +83,7 @@ public class AutoTool extends Module { } private int findBestHotbarTool(BlockPos pos) { + Minecraft mc = Minecraft.getInstance(); if (mc.player == null || mc.level == null) return -1; BlockState state = mc.level.getBlockState(pos); if (state.isAir()) return -1; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/ClickPearl.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/ClickPearl.java index 595d7ce..5a1d96a 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/ClickPearl.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/ClickPearl.java @@ -29,7 +29,6 @@ import dev.loki.lovisual.util.player.inventory.InventorySwap; public class ClickPearl extends Module { private static final String ACTION_CLICKPEARL = "clickpearl"; - private final Minecraft client = Minecraft.getInstance(); private ItemStack previousHeld = ItemStack.EMPTY; public ClickPearl() { @@ -43,6 +42,7 @@ public class ClickPearl extends Module { @Override public void onTick() { if (!isEnabled()) return; + Minecraft client = Minecraft.getInstance(); if (client.player == null || client.gameMode == null) return; if (isActionPressedOnce(ACTION_CLICKPEARL)) { @@ -54,6 +54,7 @@ public class ClickPearl extends Module { // ТВОЯ ЛОГИКА — НЕ ТРОНУТА // =========================== private void tryThrowPearl() { + Minecraft client = Minecraft.getInstance(); LocalPlayer player = client.player; MultiPlayerGameMode manager = client.gameMode; if (player == null || manager == null) return; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/NoDelay.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/NoDelay.java index f406342..d087ded 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/NoDelay.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/NoDelay.java @@ -99,8 +99,6 @@ public final class NoDelay extends Module { visibleWhen(num("noDelayFastBreakProgress", SETTING_FAST_BREAK_PROGRESS, 0.5f, 0.1f, 1.0f), () -> types.get(TYPE_FAST_BREAK)); - private final Minecraft mc = Minecraft.getInstance(); - private long pressStartThrow = 0L; private long lastThrow = 0L; private long pressStartInteract = 0L; @@ -122,6 +120,7 @@ public final class NoDelay extends Module { @Override public void onTick() { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc == null || mc.player == null) { resetBreakBoost(); return; @@ -162,6 +161,7 @@ public final class NoDelay extends Module { } public boolean handleFastUse() { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled()) return false; if (mc == null || mc.player == null || mc.gameMode == null) return false; if (!types.get(TYPE_FAST_THROW) && !types.get(TYPE_FAST_INTERACT)) return false; @@ -214,6 +214,7 @@ public final class NoDelay extends Module { } private void applyBlockPlaceCooldown() { + Minecraft mc = Minecraft.getInstance(); if (mc.player == null || !mc.options.keyUse.isDown()) return; if (!isBlockPlaceCandidate(mc.player, mc.hitResult)) return; @@ -233,6 +234,7 @@ public final class NoDelay extends Module { } private void applyFastBreakBoost() { + Minecraft mc = Minecraft.getInstance(); if (mc.gameMode == null || mc.getConnection() == null) { resetBreakBoost(); return; @@ -282,6 +284,7 @@ public final class NoDelay extends Module { } private Direction resolveBreakingDirection(BlockPos breakingPos) { + Minecraft mc = Minecraft.getInstance(); HitResult hit = mc.hitResult; if (hit instanceof BlockHitResult blockHit && breakingPos.equals(blockHit.getBlockPos())) { return blockHit.getDirection(); @@ -319,6 +322,7 @@ public final class NoDelay extends Module { } private InteractionResult performFastUse(LocalPlayer player, UseContext ctx) { + Minecraft mc = Minecraft.getInstance(); if (ctx.target == UseTarget.THROWABLE) { return mc.gameMode.useItem(player, ctx.hand); } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/ThirdPerson.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/ThirdPerson.java new file mode 100644 index 0000000..d06d78f --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/ThirdPerson.java @@ -0,0 +1,56 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.player; + +import dev.loki.lovisual.config.values.BindMode; +import dev.loki.lovisual.config.values.NumberValue; +import dev.loki.lovisual.features.module.Module; +import dev.loki.lovisual.features.module.ModuleCategory; +import dev.loki.lovisual.features.module.ModuleInfo; + +/** + * Free third-person look: while the bind is held the camera orbits freely and + * the player's movement direction stays untouched. No camera entity is + * spawned — only the vanilla third-person camera is extended. + */ +@ModuleInfo( + id = "thirdperson", + displayName = "Third Person", + aliases = {"thirdperson", "3p"}, + category = ModuleCategory.PLAYER +) +public class ThirdPerson extends Module { + private static final String SETTING_LOOK = "look"; + private static final String SETTING_DISTANCE = "distance"; + + private final NumberValue<Double> distance = num("thirdPersonDistance", SETTING_DISTANCE, 4.0, 1.0, 16.0); + + public float orbitYaw; + public float orbitPitch; + + { + action(SETTING_LOOK, "L", BindMode.HOLD); + } + + /** + * Camera input is captured only while the action bind is held; otherwise + * the vanilla turnPlayer path stays in charge. + */ + public boolean isLooking() { + return isEnabled() && isActionHeld(SETTING_LOOK); + } + + public double distance() { + return distance.get(); + } + + /** Pure pitch clamp helper shared with the mouse mixin. */ + public static float clampPitch(float pitch) { + return Math.max(-90.0f, Math.min(90.0f, pitch)); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/UseTracker.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/UseTracker.java new file mode 100644 index 0000000..09d2c1c --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/player/UseTracker.java @@ -0,0 +1,144 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.player; + +import dev.loki.lovisual.config.values.BooleanMapValue; +import dev.loki.lovisual.config.values.BooleanValue; +import dev.loki.lovisual.events.EventHandler; +import dev.loki.lovisual.events.impl.PacketEvent; +import dev.loki.lovisual.features.module.Module; +import dev.loki.lovisual.features.module.ModuleCategory; +import dev.loki.lovisual.features.module.ModuleInfo; +import dev.loki.lovisual.features.module.Notifier; +import dev.loki.lovisual.util.protection.NotifyThrottle; +import net.minecraft.util.Util; +import net.minecraft.client.Minecraft;import net.minecraft.client.player.LocalPlayer; +import net.minecraft.core.Holder; +import net.minecraft.network.protocol.game.ClientboundEntityEventPacket; +import net.minecraft.network.protocol.game.ClientboundUpdateMobEffectPacket; +import net.minecraft.world.effect.MobEffect; +import net.minecraft.world.effect.MobEffects; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Items; + +import java.util.Map; + +/** + * Announces notable item/effect uses in the HUD: totem pops (self and + * opponents) and selected combat effects applied to opponents. All decisions + * are packet-driven, so nothing is missed by render-tick sampling. + */ +@ModuleInfo( + id = "usetracker", + displayName = "Use Tracker", + aliases = {"usetracker"}, + category = ModuleCategory.PLAYER +) +public class UseTracker extends Module { + static final byte TOTEM_POP_EVENT_ID = 35; + + private static final String SETTING_SELF_TOTEMS = "self_totems"; + private static final String SETTING_OTHER_TOTEMS = "other_totems"; + private static final String SETTING_OTHER_EFFECTS = "other_effects"; + + private final BooleanValue selfTotems = bool("useTrackerSelfTotems", SETTING_SELF_TOTEMS, true); + private final BooleanValue otherTotems = bool("useTrackerOtherTotems", SETTING_OTHER_TOTEMS, true); + private final BooleanMapValue otherEffects = group("useTrackerOtherEffects", SETTING_OTHER_EFFECTS, + UseEffects.defaultOptions()); + + private final NotifyThrottle throttle = new NotifyThrottle(300L); + + @EventHandler + private void onPacket(PacketEvent.Receive event) { + if (event.getPacket() instanceof ClientboundEntityEventPacket entityEvent) { + handleEntityEvent(entityEvent); + } else if (event.getPacket() instanceof ClientboundUpdateMobEffectPacket effectPacket) { + handleEffectPacket(effectPacket); + } + } + + private void handleEntityEvent(ClientboundEntityEventPacket packet) { + if (packet.getEventId() != TOTEM_POP_EVENT_ID) return; + + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.level == null) return; + Entity entity = packet.getEntity(mc.level); + if (!(entity instanceof Player player)) return; + + if (player instanceof LocalPlayer) { + if (!selfTotems.get() || !throttle.tryAcquire(Util.getMillis())) return; + Notifier.warning("Totem popped: " + countTotems(mc.player) + " left"); + } else if (otherTotems.get()) { + Notifier.info(player.getGameProfile().name() + " popped a totem"); + } + } + + private void handleEffectPacket(ClientboundUpdateMobEffectPacket packet) { + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.level == null) return; + Entity entity = mc.level.getEntity(packet.getEntityId()); + if (!(entity instanceof Player player) || player instanceof LocalPlayer) return; + + String key = UseEffects.optionKey(packet.getEffect()); + if (key == null || !otherEffects.get(key)) return; + + Notifier.info(player.getGameProfile().name() + " used " + UseEffects.displayName(key)); + } + + private static int countTotems(Player player) { + int count = 0; + var inventory = player.getInventory(); + for (int i = 0; i < inventory.getContainerSize(); i++) { + if (inventory.getItem(i).is(Items.TOTEM_OF_UNDYING)) count++; + } + return count; + } + + /** + * Pure effect bookkeeping: which combat effects are trackable and how they + * are displayed. Holder-to-key mapping happens in the module. + */ + public static final class UseEffects { + private UseEffects() { + } + + public static Map<String, Boolean> defaultOptions() { + return Map.of( + "invisibility", true, + "speed", false, + "strength", false, + "regeneration", false, + "fire_resistance", false, + "weakness", false + ); + } + + static String optionKey(Holder<MobEffect> effect) { + if (effect == MobEffects.INVISIBILITY) return "invisibility"; + if (effect == MobEffects.SPEED) return "speed"; + if (effect == MobEffects.STRENGTH) return "strength"; + if (effect == MobEffects.REGENERATION) return "regeneration"; + if (effect == MobEffects.FIRE_RESISTANCE) return "fire_resistance"; + if (effect == MobEffects.WEAKNESS) return "weakness"; + return null; + } + + static String displayName(String key) { + return switch (key) { + case "invisibility" -> "Invisibility"; + case "speed" -> "Speed"; + case "strength" -> "Strength"; + case "regeneration" -> "Regeneration"; + case "fire_resistance" -> "Fire Resistance"; + case "weakness" -> "Weakness"; + default -> "an effect"; + }; + } + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/BlockESP.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/BlockESP.java index ac3a08e..8a5d322 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/BlockESP.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/BlockESP.java @@ -35,8 +35,6 @@ import java.util.concurrent.atomic.AtomicBoolean; ) public final class BlockESP extends Module { - private final Minecraft mc = Minecraft.getInstance(); - private final ItemIdSetValue blockList = itemList("block_list", TextListSetting.PickerMode.BLOCKS); @@ -85,6 +83,7 @@ public final class BlockESP extends Module { @Override public void onTick() { + Minecraft mc = Minecraft.getInstance(); if (mc.level == null || mc.player == null) return; if (scanTimer.passedS(scanInterval.get())) { @@ -100,6 +99,7 @@ public final class BlockESP extends Module { @Override public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.level == null || mc.player == null) return; if (blockList.get().isEmpty()) return; @@ -115,6 +115,7 @@ public final class BlockESP extends Module { } private void requestScan() { + Minecraft mc = Minecraft.getInstance(); if (mc.level == null || mc.player == null) return; if (!scanning.get()) return; if (executor.isShutdown()) return; @@ -130,6 +131,7 @@ public final class BlockESP extends Module { } private void scan() { + Minecraft mc = Minecraft.getInstance(); if (mc.level == null || mc.player == null) return; List<BlockPos> result = new ArrayList<>(); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/BlockHighlight.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/BlockHighlight.java index f3dde5a..81a0f57 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/BlockHighlight.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/BlockHighlight.java @@ -45,7 +45,6 @@ public class BlockHighlight extends Module { private static final double RAINBOW_SPATIAL_SCALE = 12.0; private static final double GEOMETRY_EXPAND = 0.0025; - private final Minecraft mc = Minecraft.getInstance(); private final BooleanValue outlineEnabled = bool( "outline_enabled", @@ -240,6 +239,7 @@ public class BlockHighlight extends Module { @Override public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.level == null || mc.player == null) return; if (outlineEnabled.get()) { @@ -252,6 +252,7 @@ public class BlockHighlight extends Module { } private void renderTargetOutline(Renderer3D renderer) { + Minecraft mc = Minecraft.getInstance(); HitResult hit = mc.hitResult; if (!(hit instanceof BlockHitResult blockHit)) return; if (hit.getType() == HitResult.Type.MISS) return; @@ -417,6 +418,7 @@ public class BlockHighlight extends Module { } private void renderBreakingHighlight(Renderer3D renderer) { + Minecraft mc = Minecraft.getInstance(); if (breakingSelf.get()) { renderSelfBreaking(renderer); } @@ -441,6 +443,7 @@ public class BlockHighlight extends Module { } private void renderSelfBreaking(Renderer3D renderer) { + Minecraft mc = Minecraft.getInstance(); MultiPlayerGameMode interactionManager = mc.gameMode; if (interactionManager == null || !interactionManager.isDestroying()) return; if (!(interactionManager instanceof MultiPlayerGameModeAccessor accessor)) return; @@ -453,6 +456,7 @@ public class BlockHighlight extends Module { } private void renderBreakingInfo(Renderer3D renderer, BlockPos pos, float progress) { + Minecraft mc = Minecraft.getInstance(); if (mc.level == null || pos == null) return; BlockState state = mc.level.getBlockState(pos); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/Chams.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/Chams.java index 17e5137..ced84da 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/Chams.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/Chams.java @@ -123,7 +123,6 @@ public class Chams extends Module { private static final float NOISE_CONTRAST = 1.15f; private static final float NOISE_BREATHE = 0.55f; private static final float NOISE_DENSITY = 1.0f; - private final Minecraft mc = Minecraft.getInstance(); private final ModeValue mode = modeSetting("handChamsMode", SETTING_MODE, "Smoke", "Smoke", "Metallic", "Glass", "Noise", "Snow", "Chroma", "Solid", "Stripes", "Glow", "Balatro"); @@ -297,6 +296,7 @@ public class Chams extends Module { } public boolean isActive() { + Minecraft mc = Minecraft.getInstance(); return isEnabled() && mc.player != null && mc.level != null; } @@ -307,6 +307,7 @@ public class Chams extends Module { } private FeatureRenderDispatcher getStandaloneHandFeatureDispatcher() { + Minecraft mc = Minecraft.getInstance(); if (irisHandFeatureDispatcher != null) { return irisHandFeatureDispatcher; } @@ -342,6 +343,7 @@ public class Chams extends Module { } public boolean renderHandMask(GameRenderer renderer, CameraRenderState cameraRenderState, float tickDelta, Matrix4fc positionMatrix) { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.player == null || mc.level == null) return false; if (mc.gameMode == null) return false; if (!hands.get() || !shouldRenderHand()) return false; @@ -385,6 +387,7 @@ public class Chams extends Module { } public boolean renderPreparedHandScene(FeatureRenderDispatcher dispatcher, SubmitNodeStorage storage) { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.player == null || mc.level == null) return false; if (dispatcher == null || storage == null) return false; if (!hands.get() || !shouldRenderHand()) return false; @@ -433,6 +436,7 @@ public class Chams extends Module { CameraRenderState cameraRenderState, Matrix4fc positionMatrix, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); if (!IrisRuntime.isShaderpackRendererActive() || !isEnabled() || mc.player == null || mc.level == null) { return false; } @@ -488,6 +492,7 @@ public class Chams extends Module { FeatureRenderDispatcher dispatcher, float tickDelta, boolean solidPhase) { + Minecraft mc = Minecraft.getInstance(); IrisHandRendererAccessor irisHand = (IrisHandRendererAccessor) HandRenderer.INSTANCE; irisHand.lovisual$setRenderingSolid(solidPhase); @@ -523,6 +528,7 @@ public class Chams extends Module { } private void ensureBuffers() { + Minecraft mc = Minecraft.getInstance(); int w = mc.getWindow().getWidth(); int h = mc.getWindow().getHeight(); if (w <= 0 || h <= 0) return; @@ -612,6 +618,7 @@ public class Chams extends Module { } private void ensureDualBuffers() { + Minecraft mc = Minecraft.getInstance(); int w = mc.getWindow().getWidth(); int h = mc.getWindow().getHeight(); if (w <= 0 || h <= 0) return; @@ -665,6 +672,7 @@ public class Chams extends Module { } private boolean renderHands(GpuTextureView src, GpuTextureView dst, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); if (!maskReady) return false; if (handMask == null) { maskReady = false; @@ -792,6 +800,7 @@ public class Chams extends Module { } private void renderChamsEffect(GpuTextureView src, GpuTextureView dst, GpuTextureView maskView, String chamsMode, float rawTime) { + Minecraft mc = Minecraft.getInstance(); int w = mc.getWindow().getWidth(); int h = mc.getWindow().getHeight(); float time = rawTime; @@ -862,6 +871,7 @@ public class Chams extends Module { } private void renderNoise(GpuTextureView src, GpuTextureView dst, GpuTextureView maskView, float rawTime) { + Minecraft mc = Minecraft.getInstance(); int fillRgb = materialFillRgb(); int glowRgb = glowRgb(fillRgb); @@ -941,6 +951,7 @@ public class Chams extends Module { private final class HandsPass implements PostProcessPass { @Override public boolean isActive() { + Minecraft mc = Minecraft.getInstance(); return Chams.this.isEnabled() && mc.player != null && mc.level != null @@ -955,6 +966,7 @@ public class Chams extends Module { @Override public boolean render(GpuTextureView src, GpuTextureView dst, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); if (!Chams.this.isEnabled() || mc.player == null || mc.level == null) return false; if (!hands.get() || !mc.options.getCameraType().isFirstPerson()) return false; return Chams.this.renderHands(src, dst, tickDelta); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/ChinaHat.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/ChinaHat.java index 6e59627..ca9089f 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/ChinaHat.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/ChinaHat.java @@ -25,8 +25,9 @@ import net.minecraft.client.Minecraft; import net.minecraft.world.entity.player.Player; /** - * Classic conical Chinese hat (rice hat / coolie hat): a wide flat brim annulus - * topped by a cone dome. Geometry lives in {@link ChinaHatModel}, colors in + * Classic conical Chinese hat (rice hat / coolie hat): a wide shallow cone — + * apex on top, rim circle around the crown — matching the SoupVisuals profile + * (radius:height ≈ 2:1). Geometry lives in {@link ChinaHatModel}, colors in * {@link ChinaHatPalette}; this class only wires settings to rendering. */ @ModuleInfo( @@ -36,8 +37,6 @@ import net.minecraft.world.entity.player.Player; ) public final class ChinaHat extends Module { - private final Minecraft mc = Minecraft.getInstance(); - private final EnumValue<RenderTarget> renderTarget = enumMode( "render_target", @@ -115,30 +114,16 @@ public final class ChinaHat extends Module { private final NumberValue<Float> brimRadius = num( "brim_radius", - 0.45f, + 0.5f, 0.2f, - 0.7f + 0.9f ); - private final NumberValue<Float> domeRadius = + private final NumberValue<Float> coneHeight = num( - "dome_radius", - 0.16f, - 0.06f, - 0.35f - ); - private final NumberValue<Float> domeHeight = - num( - "dome_height", - 0.3f, - 0.1f, - 0.55f - ); - private final NumberValue<Float> brimThickness = - num( - "brim_thickness", - 0.025f, - 0.01f, - 0.08f + "cone_height", + 0.22f, + 0.05f, + 0.6f ); private final NumberValue<Float> headTopOffset = num( @@ -169,6 +154,7 @@ public final class ChinaHat extends Module { @Override public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.level == null || mc.player == null) return; MeshBuilder tris = renderer.batch( @@ -245,14 +231,11 @@ public final class ChinaHat extends Module { anchor, segments.get(), brimRadius.get() * size, - domeRadius.get() * size, - domeHeight.get() * size, - brimThickness.get() * size, + coneHeight.get() * size, 0.0f, 0.0f, 0.0f, (index, light, alphaMul) -> ChinaHatPalette.colorAt(palette, mode, speed, index, light, alphaMul), - (index, light, alphaMul) -> ChinaHatPalette.colorAt(palette, mode, speed, index, light, alphaMul), (index, light, alphaMul) -> ChinaHatPalette.rimColorAt(palette, mode, speed, index, alphaMul), 1.0f ); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/ChinaHatModel.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/ChinaHatModel.java index 1c19781..ba8930d 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/ChinaHatModel.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/ChinaHatModel.java @@ -13,10 +13,14 @@ import net.minecraft.util.Mth; import net.minecraft.world.phys.Vec3; /** - * Geometry for the classic ChinaHat: a wide flat brim (annulus) plus a conical dome. - * Pure mesh assembly, no Minecraft state. Per-vertex colors come from the caller. + * Classic coolie/rice hat: a single smooth cone from the apex resting on the + * head up to a raised rim circle, like the SoupVisuals reference profile + * (radius 0.65 vs height 0.3). Rendered double-sided via opposite windings. + * {@link #layout} is pure geometry (unit-testable); {@link #build} feeds the + * result into MeshBuilder. Per-vertex colors come from the caller. */ final class ChinaHatModel { + private ChinaHatModel() { } @@ -24,76 +28,59 @@ final class ChinaHatModel { int color(int index, float verticalLight, float alphaMul); } + /** Apex plus rim circle vertices in anchor space; rim[i+1 wraps to rim[0]. */ + record Layout(Vec3 apex, Vec3[] rim) { + } + + static Layout layout(float brimRadius, + float coneHeight, + int segments, + float yawDeg, + float pitchDeg, + float rollDeg, + HeadAnchor anchor) { + Vec3 apex = anchor.localRotated(0.0f, coneHeight, 0.0f, yawDeg, pitchDeg, rollDeg); + Vec3[] rim = new Vec3[Math.max(0, segments)]; + for (int i = 0; i < rim.length; i++) { + double angle = (Math.PI * 2.0 * i) / segments; + float s = Mth.sin((float) angle); + float c = Mth.cos((float) angle); + rim[i] = anchor.localRotated(s * brimRadius, 0.0f, c * brimRadius, + yawDeg, pitchDeg, rollDeg); + } + return new Layout(apex, rim); + } + static void build(MeshBuilder tris, MeshBuilder lines, HeadAnchor anchor, int segments, float brimRadius, - float domeRadius, - float domeHeight, - float brimThickness, + float coneHeight, float yawDeg, float pitchDeg, float rollDeg, - Shade domeShade, - Shade brimShade, + Shade coneShade, Shade rimShade, float alphaMul) { - if (tris == null || lines == null || segments < 3) return; + if (tris == null || segments < 3) return; - float outerY = brimThickness; - float innerY = 0.0f; - Vec3[] outerTop = new Vec3[segments]; - Vec3[] outerBottom = new Vec3[segments]; - Vec3[] innerTop = new Vec3[segments]; - Vec3[] innerBottom = new Vec3[segments]; - - for (int i = 0; i < segments; i++) { - double angle = (Math.PI * 2.0 * i) / segments; - float s = Mth.sin((float) angle); - float c = Mth.cos((float) angle); - outerTop[i] = anchor.localRotated(s * brimRadius, outerY, c * brimRadius, yawDeg, pitchDeg, rollDeg); - outerBottom[i] = anchor.localRotated(s * brimRadius, innerY, c * brimRadius, yawDeg, pitchDeg, rollDeg); - innerTop[i] = anchor.localRotated(s * domeRadius, outerY, c * domeRadius, yawDeg, pitchDeg, rollDeg); - innerBottom[i] = anchor.localRotated(s * domeRadius, innerY, c * domeRadius, yawDeg, pitchDeg, rollDeg); - } - - Vec3 tip = anchor.localRotated(0.0f, outerY + domeHeight, 0.0f, yawDeg, pitchDeg, rollDeg); + Layout layout = layout(brimRadius, coneHeight, segments, yawDeg, pitchDeg, rollDeg, anchor); + Vec3 apex = layout.apex(); + Vec3[] rim = layout.rim(); for (int i = 0; i < segments; i++) { int next = (i + 1) % segments; - int domeA = domeShade.color(i, 1.06f, alphaMul); - int domeB = domeShade.color(next, 1.02f, alphaMul); - triangle(tris, tip, innerTop[i], domeA, innerTop[next], domeB); + int upperA = coneShade.color(i, 1.06f, alphaMul); + int upperB = coneShade.color(next, 1.02f, alphaMul); + triangle(tris, apex, rim[i], upperA, rim[next], upperB); + triangle(tris, apex, rim[next], upperA, rim[i], upperB); - int brimTopA = brimShade.color(i, 0.94f, alphaMul); - int brimTopB = brimShade.color(next, 0.90f, alphaMul); - quad(tris, - innerTop[i], brimTopA, - innerTop[next], brimTopB, - outerTop[next], brimTopB, - outerTop[i], brimTopA); - - int brimBotA = brimShade.color(i, 0.42f, alphaMul); - int brimBotB = brimShade.color(next, 0.40f, alphaMul); - quad(tris, - outerBottom[i], brimBotA, - outerBottom[next], brimBotB, - innerBottom[next], brimBotA, - innerBottom[i], brimBotB); - - int edgeA = brimShade.color(i, 0.72f, alphaMul); - int edgeB = brimShade.color(next, 0.70f, alphaMul); - quad(tris, - outerBottom[i], edgeA, - outerBottom[next], edgeB, - outerTop[next], edgeB, - outerTop[i], edgeA); - - int rim1 = rimShade.color(i, 1.0f, alphaMul); - int rim2 = rimShade.color(next, 1.0f, alphaMul); - lineGradient(lines, outerTop[i], outerTop[next], rim1, rim2); + if (lines == null) continue; + int rimA = rimShade.color(i, 1.0f, alphaMul); + int rimB = rimShade.color(next, 1.0f, alphaMul); + lineGradient(lines, rim[i], rim[next], rimA, rimB); } } @@ -105,19 +92,6 @@ final class ChinaHatModel { mesh.triangle(i1, i2, i3); } - private static void quad(MeshBuilder mesh, - Vec3 p1, int c1, - Vec3 p2, int c2, - Vec3 p3, int c3, - Vec3 p4, int c4) { - mesh.ensureQuadCapacity(); - int i1 = putVertex(mesh, p1, c1); - int i2 = putVertex(mesh, p2, c2); - int i3 = putVertex(mesh, p3, c3); - int i4 = putVertex(mesh, p4, c4); - mesh.quad(i1, i2, i3, i4); - } - private static void lineGradient(MeshBuilder mesh, Vec3 a, Vec3 b, int cA, int cB) { mesh.ensureLineCapacity(); int i1 = putVertex(mesh, a, cA); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/CosmeticDisplay.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/CosmeticDisplay.java index 4490b5f..536082f 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/CosmeticDisplay.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/CosmeticDisplay.java @@ -27,8 +27,6 @@ import java.util.List; ) public final class CosmeticDisplay extends Module { - private final Minecraft mc = Minecraft.getInstance(); - { action("Next Cosmetic", "NONE"); action("Prev Cosmetic", "NONE"); @@ -68,6 +66,7 @@ public final class CosmeticDisplay extends Module { @Override public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.level == null || mc.player == null) return; CosmeticRenderer.renderOnPlayer(renderer, mc.player, CosmeticType.MODEL, tickDelta); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/Crosshair.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/Crosshair.java index 2db18cc..96e14f6 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/Crosshair.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/Crosshair.java @@ -68,7 +68,6 @@ public class Crosshair extends Module { private static final float ORBIZ_PROGRESS_SMOOTH_SPEED = 22.0f; private static final float COOLDOWN_LEAD = 0.06f; private static final float RIPTIDE_CROSS_LENGTH_FACTOR = 0.65f; - private final Minecraft mc = Minecraft.getInstance(); private final EnumValue<CrosshairType> crosshairType = enumSetting("crosshairType", "type", CrosshairType.CLASSIC); private final EnumValue<OrbizMotionMode> orbizMotion = @@ -241,6 +240,7 @@ public class Crosshair extends Module { } public boolean shouldHideVanilla() { + Minecraft mc = Minecraft.getInstance(); return isEnabled() && mc.player != null && mc.level != null @@ -263,6 +263,7 @@ public class Crosshair extends Module { @Override public void onRenderHudEngine(Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.player == null || mc.level == null) return; if (!mc.options.getCameraType().isFirstPerson()) return; @@ -336,6 +337,7 @@ public class Crosshair extends Module { } private float getAttackCooldownProgress() { + Minecraft mc = Minecraft.getInstance(); if (mc.player instanceof IPlayerAttackCooldown cooldown) { return cooldown.lovisual$getAttackCooldownProgress(0.0f); } @@ -366,6 +368,7 @@ public class Crosshair extends Module { } private void renderOrbiz(Renderer2D renderer, float centerX, float centerY, float scale) { + Minecraft mc = Minecraft.getInstance(); boolean danger = shouldShowRiptideBlocked(mc.player) || shouldShowCooldownCross(mc.player); float progress = danger ? 1.0f : resolveOrbizProgress(); float targetProgress = Mth.clamp(progress * orbizCooldownMultiplier.get().floatValue(), 0.0f, 1.0f); @@ -414,6 +417,7 @@ public class Crosshair extends Module { } private float resolveOrbizProgress() { + Minecraft mc = Minecraft.getInstance(); float charge = getChargeProgress(mc.player); if (charge >= 0.0f) { return Mth.clamp(charge, 0.0f, 1.0f); @@ -422,6 +426,7 @@ public class Crosshair extends Module { } private void updateOrbizMotion() { + Minecraft mc = Minecraft.getInstance(); if (mc.player == null) { orbizOffsetX = AnimationUtility.fast(orbizOffsetX, 0.0f, 10.0f); orbizOffsetY = AnimationUtility.fast(orbizOffsetY, 0.0f, 10.0f); @@ -490,6 +495,7 @@ public class Crosshair extends Module { } private int resolveCrosshairTargetColor() { + Minecraft mc = Minecraft.getInstance(); if (!highlightTarget.get()) return DEFAULT_COLOR; HitResult hit = mc.hitResult; if (!(hit instanceof EntityHitResult ehr)) return DEFAULT_COLOR; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/DropESP.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/DropESP.java index 6425aa2..229ab31 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/DropESP.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/DropESP.java @@ -58,7 +58,6 @@ public class DropESP extends Module { private static final double MATTE_ICON_SIZE = 14.0; private static final double MATTE_ICON_GAP = 4.0; private static final float MATTE_ICON_SCALE = (float) (MATTE_ICON_SIZE / 16.0); - private final Minecraft mc = Minecraft.getInstance(); private final ModeValue modeValue = modeSetting("dropEspMode", SETTING_MODE, "Matte", "Vanilla", "New", "Matte"); private final BooleanValue limitCommonDistanceValue = bool("dropEspLimitCommonDistance", SETTING_LIMIT_COMMON_DISTANCE, true); private final NumberValue<Integer> commonMaxDistanceValue = @@ -98,6 +97,7 @@ public class DropESP extends Module { @Override public void onRenderHudEngine(Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.level == null || mc.player == null) return; boolean matteMode = isMatteMode(); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/ESP.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/ESP.java index 369f24f..1d63692 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/ESP.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/ESP.java @@ -62,7 +62,6 @@ public class ESP extends Module { static final double SHADER_MAX_DISTANCE = 128.0; static final float BOX_3D_GRADIENT_SPEED = 0.00115f; - final Minecraft mc = Minecraft.getInstance(); final BooleanValue renderEntities = bool("espRenderEntities", "render_entities", false); final RGBColorValue entityColor = @@ -185,6 +184,7 @@ public class ESP extends Module { @Override public void onRenderHudEngine(Renderer2D renderer, TextRenderer textRenderer, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.level == null || mc.player == null) return; Vec3 camPos = mc.gameRenderer.mainCamera().position(); @@ -249,6 +249,7 @@ public class ESP extends Module { private final class ShaderEspPass implements PostProcessPass { @Override public boolean isActive() { + Minecraft mc = Minecraft.getInstance(); return ESP.this.isEnabled() && ESP.this.isShaderBox() && mc.player != null diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/Esp3DRenderer.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/Esp3DRenderer.java index 3e5c7b2..826546d 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/Esp3DRenderer.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/Esp3DRenderer.java @@ -11,6 +11,7 @@ import dev.loki.lovisual.render.engine.RenderState; import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; import dev.loki.lovisual.render.engine.renderer.Renderer3D; import dev.loki.lovisual.render.engine.uniform.MeshBuilder; +import net.minecraft.client.Minecraft; import net.minecraft.world.entity.Entity; import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.Vec3; @@ -24,9 +25,10 @@ final class Esp3DRenderer { } static void render(ESP esp, Renderer3D renderer, float tickDelta) { - if (!esp.isEnabled() || !esp.is3DBox() || esp.mc.level == null || esp.mc.player == null) return; + Minecraft mc = Minecraft.getInstance(); + if (!esp.isEnabled() || !esp.is3DBox() || mc.level == null || mc.player == null) return; - Vec3 camPos = esp.mc.gameRenderer.mainCamera().position(); + Vec3 camPos = mc.gameRenderer.mainCamera().position(); List<EspModel.RenderEntry> entries = EspEntityCollector.collectPlayerEntries(esp, camPos); if (entries.isEmpty()) return; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/EspEntityCollector.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/EspEntityCollector.java index 396c59f..f70be23 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/EspEntityCollector.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/EspEntityCollector.java @@ -8,6 +8,7 @@ package dev.loki.lovisual.features.module.modules.visuals; import dev.loki.lovisual.features.relations.CategoryService; +import net.minecraft.client.Minecraft; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.resources.Identifier; import net.minecraft.world.entity.Entity; @@ -45,9 +46,10 @@ final class EspEntityCollector { } static List<EspModel.RenderEntry> collectPlayerEntries(ESP esp, Vec3 camPos) { + Minecraft mc = Minecraft.getInstance(); List<EspModel.RenderEntry> entries = new ArrayList<>(); - for (Player player : esp.mc.level.players()) { + for (Player player : mc.level.players()) { if (!shouldRenderPlayer(esp, player)) continue; int color = resolvePlayerColor(player); double distSq = player.distanceToSqr(camPos); @@ -58,11 +60,12 @@ final class EspEntityCollector { } static List<EspModel.RenderEntry> collectSearchEntityEntries(ESP esp, Vec3 camPos) { + Minecraft mc = Minecraft.getInstance(); List<EspModel.RenderEntry> entries = new ArrayList<>(); if (!esp.renderEntities.get()) return entries; int color = esp.entityColor.getArgb(); - for (Entity entity : esp.mc.level.entitiesForRendering()) { + for (Entity entity : mc.level.entitiesForRendering()) { if (!shouldRenderEntity(esp, entity)) continue; double distSq = entity.distanceToSqr(camPos); entries.add(new EspModel.RenderEntry(entity, color, distSq)); @@ -91,6 +94,7 @@ final class EspEntityCollector { } static List<EspModel.RenderEntry> collectShaderChamsEntityEntries(ESP esp, Vec3 camPos, double maxDistSq) { + Minecraft mc = Minecraft.getInstance(); List<EspModel.RenderEntry> entries = new ArrayList<>(); Set<String> selected = esp.shaderChamsEntities.get(); if (selected == null || selected.isEmpty()) { @@ -98,7 +102,7 @@ final class EspEntityCollector { } int color = esp.shaderChamsEntityColor.getArgb(); - for (Entity entity : esp.mc.level.entitiesForRendering()) { + for (Entity entity : mc.level.entitiesForRendering()) { if (!shouldRenderShaderChamsEntity(esp, entity, selected)) { continue; } @@ -112,9 +116,10 @@ final class EspEntityCollector { } static boolean shouldRenderPlayer(ESP esp, Player p) { + Minecraft mc = Minecraft.getInstance(); if (p == null) return false; - if (p == esp.mc.player) { - if (esp.mc.options.getCameraType().isFirstPerson()) return false; + if (p == mc.player) { + if (mc.options.getCameraType().isFirstPerson()) return false; return esp.renderSelf.get(); } if (esp.skipInvisible.get() && (p.isInvisible() || p.hasEffect(net.minecraft.world.effect.MobEffects.INVISIBILITY))) { @@ -137,32 +142,35 @@ final class EspEntityCollector { } static boolean hasLineOfSight(ESP esp, Player p) { - if (esp.mc.level == null) return false; - Vec3 start = esp.mc.gameRenderer.mainCamera().position(); + Minecraft mc = Minecraft.getInstance(); + if (mc.level == null) return false; + Vec3 start = mc.gameRenderer.mainCamera().position(); Vec3 end = p.getBoundingBox().getCenter(); - HitResult result = esp.mc.level.clip(new ClipContext( + HitResult result = mc.level.clip(new ClipContext( start, end, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, - esp.mc.player + mc.player )); return result.getType() == HitResult.Type.MISS; } static boolean shouldRenderEntity(ESP esp, Entity e) { + Minecraft mc = Minecraft.getInstance(); if (e == null) return false; if (e instanceof Player) return false; - if (e == esp.mc.player) return false; + if (e == mc.player) return false; Identifier id = BuiltInRegistries.ENTITY_TYPE.getKey(e.getType()); if (id == null) return false; return esp.searchEntities.get().contains(id.toString()); } static boolean shouldRenderShaderChamsEntity(ESP esp, Entity entity, Set<String> selected) { + Minecraft mc = Minecraft.getInstance(); if (entity == null) return false; if (entity instanceof Player) return false; - if (entity == esp.mc.player) return false; + if (entity == mc.player) return false; Identifier id = BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()); if (id == null) return false; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/EspShaderMask.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/EspShaderMask.java index 78549b8..bebb979 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/EspShaderMask.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/EspShaderMask.java @@ -24,6 +24,7 @@ import dev.loki.lovisual.render.engine.core.LoVisualRenderSystem; import dev.loki.lovisual.render.engine.msaa.MsaaFramebuffer; import dev.loki.lovisual.render.engine.msaa.MsaaWorldTarget; import dev.loki.lovisual.render.engine.renderer.MeshRenderer; +import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.feature.FeatureRenderDispatcher; import net.minecraft.client.renderer.LevelRenderer; import net.minecraft.client.renderer.ProjectionMatrixBuffer; @@ -46,11 +47,12 @@ final class EspShaderMask { } static boolean renderEntityMask(ESP esp, List<EspModel.RenderEntry> entries, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); if (entries.isEmpty() || esp.shaderMask == null) { return false; } - LevelRenderer worldRenderer = esp.mc.levelRenderer; + LevelRenderer worldRenderer = mc.levelRenderer; if (worldRenderer == null) { return false; } @@ -103,8 +105,8 @@ final class EspShaderMask { Matrix4f capturedPosition = LoVisualWorldMatrices.positionMatrix(); if (capturedPosition != null) { modelView.mul(capturedPosition); - } else if (esp.mc.gameRenderer != null && esp.mc.gameRenderer.mainCamera() != null) { - modelView.mul(new Matrix4f().rotation(esp.mc.gameRenderer.mainCamera().rotation().conjugate(new Quaternionf()))); + } else if (mc.gameRenderer != null && mc.gameRenderer.mainCamera() != null) { + modelView.mul(new Matrix4f().rotation(mc.gameRenderer.mainCamera().rotation().conjugate(new Quaternionf()))); } PoseStack matrices = new PoseStack(); @@ -122,7 +124,7 @@ final class EspShaderMask { esp.shaderEspCommandQueue.getSubmitsPerOrder().clear(); - EntityRenderDispatcher entityRenderManager = esp.mc.getEntityRenderDispatcher(); + EntityRenderDispatcher entityRenderManager = mc.getEntityRenderDispatcher(); for (EspModel.RenderEntry entry : entries) { Entity entity = entry.entity(); EntityCullingCompat.forceVisibleForShaderEsp(entity); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/EspShaderRenderer.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/EspShaderRenderer.java index b736cde..613f337 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/EspShaderRenderer.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/EspShaderRenderer.java @@ -14,6 +14,7 @@ import dev.loki.lovisual.render.engine.msaa.MsaaFramebuffer; import dev.loki.lovisual.render.engine.msaa.MsaaWorldTarget; import dev.loki.lovisual.render.engine.postprocess.PostProcessManager; import dev.loki.lovisual.render.iris.IrisLoVisualFrameHooks; +import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.RenderBuffers; import net.minecraft.client.renderer.feature.FeatureRenderDispatcher; import net.minecraft.world.phys.Vec3; @@ -26,14 +27,15 @@ final class EspShaderRenderer { } static boolean render(ESP esp, GpuTextureView src, GpuTextureView dst, float tickDelta) { - if (!esp.isEnabled() || !esp.isShaderBox() || esp.mc.level == null || esp.mc.player == null) { + Minecraft mc = Minecraft.getInstance(); + if (!esp.isEnabled() || !esp.isShaderBox() || mc.level == null || mc.player == null) { return false; } if (!esp.shaderGlow.get() && !esp.shaderOutline.get() && !esp.shaderFill.get()) { return false; } - Vec3 camPos = esp.mc.gameRenderer.mainCamera().position(); + Vec3 camPos = mc.gameRenderer.mainCamera().position(); List<EspModel.RenderEntry> entries = EspEntityCollector.collectShaderEntries(esp, camPos); if (entries.isEmpty()) { return false; @@ -50,14 +52,15 @@ final class EspShaderRenderer { PostProcessManager.copy(src, dst); - int width = esp.mc.getWindow().getWidth(); - int height = esp.mc.getWindow().getHeight(); + int width = mc.getWindow().getWidth(); + int height = mc.getWindow().getHeight(); return EspShaderColor.renderColorPass(esp, dst, width, height); } static void ensureShaderBuffers(ESP esp) { - int width = esp.mc.getWindow().getWidth(); - int height = esp.mc.getWindow().getHeight(); + Minecraft mc = Minecraft.getInstance(); + int width = mc.getWindow().getWidth(); + int height = mc.getWindow().getHeight(); if (width <= 0 || height <= 0) { return; } @@ -96,10 +99,11 @@ final class EspShaderRenderer { } static FeatureRenderDispatcher getShaderEspRenderDispatcher(ESP esp) { + Minecraft mc = Minecraft.getInstance(); if (IrisLoVisualFrameHooks.isRenderingAfterIrisFinalization()) { return getStandaloneShaderEspRenderDispatcher(esp); } - if (esp.mc != null && esp.mc.levelRenderer instanceof dev.loki.lovisual.mixins.accessors.LevelRendererAccessor accessor) { + if (mc != null && mc.levelRenderer instanceof dev.loki.lovisual.mixins.accessors.LevelRendererAccessor accessor) { FeatureRenderDispatcher dispatcher = accessor.lovisual$getEntityRenderDispatcher(); if (dispatcher != null) { return dispatcher; @@ -109,19 +113,20 @@ final class EspShaderRenderer { } static FeatureRenderDispatcher getStandaloneShaderEspRenderDispatcher(ESP esp) { + Minecraft mc = Minecraft.getInstance(); if (esp.shaderEspRenderDispatcher != null) { return esp.shaderEspRenderDispatcher; } - if (esp.mc.gameRenderer == null || esp.mc.getModelManager() == null || esp.mc.getAtlasManager() == null || esp.mc.font == null) { + if (mc.gameRenderer == null || mc.getModelManager() == null || mc.getAtlasManager() == null || mc.font == null) { return null; } esp.shaderEspRenderBuffers = new RenderBuffers(1); esp.shaderEspRenderDispatcher = new FeatureRenderDispatcher( esp.shaderEspRenderBuffers, - esp.mc.getModelManager(), - esp.mc.getAtlasManager(), - esp.mc.font, - esp.mc.gameRenderer.gameRenderState() + mc.getModelManager(), + mc.getAtlasManager(), + mc.font, + mc.gameRenderer.gameRenderState() ); return esp.shaderEspRenderDispatcher; } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/HitEffect.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/HitEffect.java index 3a06370..daf3927 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/HitEffect.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/HitEffect.java @@ -87,7 +87,6 @@ public class HitEffect extends Module { private static final float HIT_PARTICLE_DRAG = 0.94f; private static final int HIT_PARTICLE_MAX = 320; private static final int HIT_PARTICLE_SPAWN_CAP_PER_TICK = 32; - private final Minecraft mc = Minecraft.getInstance(); private final Random random = new Random(); private final EnumValue<EffectMode> effectMode = enumSetting("hitBubblesEffectMode", SETTING_EFFECT_MODE, EffectMode.BUBBLES, EffectMode.values()); @@ -277,6 +276,7 @@ public class HitEffect extends Module { } public void handleHit(Entity target) { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.player == null || mc.level == null) return; if (!(target instanceof LivingEntity)) return; if (onlyPlayers.get() && !(target instanceof Player)) return; @@ -312,6 +312,7 @@ public class HitEffect extends Module { @Override public void onTick() { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.player == null || mc.level == null) return; if (effectMode.get() != EffectMode.BUBBLES || !starsEnabled.get()) { @@ -327,6 +328,7 @@ public class HitEffect extends Module { @Override public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.player == null || mc.level == null) return; if (hitParticlesEnabled.get() && !hitParticles.isEmpty()) { @@ -405,6 +407,7 @@ public class HitEffect extends Module { } private void addWave(BlockPos pos) { + Minecraft mc = Minecraft.getInstance(); if (mc.level != null) { waves.add(new WaveEffect(pos, GameClock.millis())); } @@ -457,6 +460,7 @@ public class HitEffect extends Module { } private int reserveHitParticleBudget(int requested) { + Minecraft mc = Minecraft.getInstance(); if (requested <= 0 || mc.player == null) { return 0; } @@ -599,6 +603,7 @@ public class HitEffect extends Module { } private Vec3 getRtxPoint(Entity target, float yaw, float pitch, double distance) { + Minecraft mc = Minecraft.getInstance(); if (mc.player == null) return null; Vec3 start = mc.player.position().add(0, mc.player.getEyeHeight(mc.player.getPose()), 0); @@ -690,6 +695,7 @@ public class HitEffect extends Module { } private void render(Renderer3D renderer) { + Minecraft mc = Minecraft.getInstance(); if (mc.level == null) return; if (renderer == null) return; @@ -751,6 +757,7 @@ public class HitEffect extends Module { } private BlockPos findSurface(BlockPos pos) { + Minecraft mc = Minecraft.getInstance(); for (int y = 2; y >= -4; y--) { BlockPos p = pos.above(y); if (!mc.level.getBlockState(p).isAir() && mc.level.getBlockState(p.above()).isAir()) { diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/Hitboxes.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/Hitboxes.java new file mode 100644 index 0000000..de271fb --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/Hitboxes.java @@ -0,0 +1,161 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.visuals; + +import dev.loki.lovisual.config.values.BooleanValue; +import dev.loki.lovisual.config.values.ModeValue; +import dev.loki.lovisual.config.values.NumberValue; +import dev.loki.lovisual.config.values.RGBColorValue; +import dev.loki.lovisual.features.module.Module; +import dev.loki.lovisual.features.module.ModuleCategory; +import dev.loki.lovisual.features.module.ModuleInfo; +import dev.loki.lovisual.features.module.WorldPhase; +import dev.loki.lovisual.features.relations.CategoryService; +import dev.loki.lovisual.features.relations.CategoryType; +import dev.loki.lovisual.render.engine.RenderState; +import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines; +import dev.loki.lovisual.render.engine.renderer.Renderer3D; +import dev.loki.lovisual.render.engine.uniform.MeshBuilder; +import net.minecraft.client.Minecraft; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; + +import java.util.ArrayList; +import java.util.List; + +/** + * Colored wireframe/fill rendering of entity hitboxes. + * Render-only: it never changes collision boxes (that would be a cheat). + */ +@ModuleInfo( + id = "hitboxes", + displayName = "Hitboxes", + category = ModuleCategory.VISUALS +) +public class Hitboxes extends Module { + + private static final String MODE_OUTLINE = "Outline"; + private static final String MODE_FILL = "Fill"; + private static final String MODE_BOTH = "Both"; + private static final String COLOR_SYNC = "Sync"; + private static final String COLOR_CUSTOM = "Custom"; + + private final ModeValue renderMode = + mode("hitboxesRenderMode", "render_mode", MODE_BOTH, MODE_OUTLINE, MODE_FILL, MODE_BOTH); + private final ModeValue colorMode = + mode("hitboxesColorMode", "color_mode", COLOR_SYNC, COLOR_SYNC, COLOR_CUSTOM); + private final BooleanValue eyesLine = bool("hitboxesEyesLine", "eyes_line", false); + private final BooleanValue playersOnly = bool("hitboxesPlayersOnly", "players_only", true); + private final NumberValue<Integer> lineWidth = num("hitboxesLineWidth", "line_width", 2, 1, 10); + private final NumberValue<Integer> fillAlpha = num("hitboxesFillAlpha", "fill_alpha", 40, 0, 255); + private final RGBColorValue hitboxColor = + visibleWhen(colorNoAlpha("hitboxesColor", "#FFFFFF"), () -> COLOR_CUSTOM.equals(colorMode.get())); + private final RGBColorValue friendHitboxColor = + visibleWhen(colorNoAlpha("hitboxesFriendColor", "#00FF00"), () -> COLOR_CUSTOM.equals(colorMode.get())); + + @Override + public WorldPhase getWorldPhase() { + return WorldPhase.END_MAIN; + } + + @Override + public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); + if (!isEnabled() || renderer == null || mc.level == null) return; + + List<Entity> targets = collectTargets(mc, tickDelta); + if (targets.isEmpty()) return; + + boolean outline = !MODE_FILL.equals(renderMode.get()); + boolean fill = !MODE_OUTLINE.equals(renderMode.get()); + + float previousLineWidth = RenderState.lineWidth; + RenderState.lineWidth = Math.max(0.01f, lineWidth.get()); + MeshBuilder lines = null; + MeshBuilder tris = null; + try { + if (outline) { + lines = renderer.batch(LoVisualRenderPipelines.WORLD_COLORED_LINES, Renderer3D.DepthMode.NONE); + } + if (fill) { + tris = renderer.batch(LoVisualRenderPipelines.WORLD_COLORED, Renderer3D.DepthMode.NONE); + } + for (Entity entity : targets) { + AABB box = interpolatedBox(entity, tickDelta); + int rgb = resolveColor(entity) & 0x00FFFFFF; + drawBox(lines, tris, box, rgb); + if (eyesLine.get() && entity instanceof LivingEntity living) { + drawEyesLine(lines, living, box, rgb); + } + } + } finally { + RenderState.lineWidth = previousLineWidth; + } + } + + private List<Entity> collectTargets(Minecraft mc, float tickDelta) { + List<Entity> out = new ArrayList<>(); + Entity camera = mc.getCameraEntity(); + for (Entity entity : mc.level.entitiesForRendering()) { + if (entity == camera) continue; + if (playersOnly.get() && !(entity instanceof Player)) continue; + if (!Renderer3D.Culling.isInFrustum(interpolatedBox(entity, tickDelta))) continue; + out.add(entity); + } + return out; + } + + static AABB interpolatedBox(Entity entity, float tickDelta) { + Vec3 pos = entity.getPosition(tickDelta); + return entity.getBoundingBox().move(pos.x - entity.getX(), pos.y - entity.getY(), pos.z - entity.getZ()); + } + + private int resolveColor(Entity entity) { + if (COLOR_SYNC.equals(colorMode.get()) && entity instanceof Player player) { + return CategoryService.getColor(player) | 0xFF000000; + } + if (entity instanceof Player player + && CategoryService.get(player) == CategoryType.FRIEND) { + return friendHitboxColor.getArgb() | 0xFF000000; + } + return hitboxColor.getArgb() | 0xFF000000; + } + + private void drawBox(MeshBuilder lines, MeshBuilder tris, AABB box, int rgb) { + if (tris != null) { + int fill = withAlpha(rgb, fillAlpha.get()); + EspMeshBuilder.addFilledBox(tris, box, fill, fill, fill, fill, fill, fill, fill, fill); + } + if (lines != null) { + int outline = 0xFF000000 | rgb; + EspMeshBuilder.addOutlineBox(lines, box, + outline, outline, outline, outline, outline, outline, outline, outline); + } + } + + private static int withAlpha(int rgb, int alpha) { + return ((alpha & 0xFF) << 24) | (rgb & 0x00FFFFFF); + } + + private void drawEyesLine(MeshBuilder lines, LivingEntity living, AABB box, int rgb) { + if (lines == null) return; + double eyeY = box.minY + living.getEyeHeight(); + double cx = (box.minX + box.maxX) / 2.0; + double cz = (box.minZ + box.maxZ) / 2.0; + double len = 0.3; + float yawDeg = living.getYRot(); + double rad = Math.toRadians(yawDeg); + double dx = -Math.sin(rad) * len; + double dz = Math.cos(rad) * len; + int argb = 0xFF000000 | rgb; + EspMeshBuilder.line(lines, cx, eyeY, cz, argb, cx + dx, eyeY + 0.05, cz + dz, argb); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/HoleESP.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/HoleESP.java index 58e1a13..f531d33 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/HoleESP.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/HoleESP.java @@ -52,8 +52,6 @@ public final class HoleESP extends Module { private static final int RESCAN_TICKS = 10; private static final int BASE_FILL_ALPHA = 60; - private final Minecraft mc = Minecraft.getInstance(); - private final NumberValue<Integer> rangeXZ = num("holeesp_range_xz", "range_xz", 10, 1, 128); private final NumberValue<Integer> rangeY = @@ -80,6 +78,7 @@ public final class HoleESP extends Module { @Override public void onTick() { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.level == null || mc.player == null) { holes = List.of(); return; @@ -97,6 +96,7 @@ public final class HoleESP extends Module { @Override public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || renderer == null || mc.level == null || mc.player == null || holes.isEmpty()) { return; } @@ -192,6 +192,7 @@ public final class HoleESP extends Module { } private int colorForDistance(AABB box, int argb, int alpha) { + Minecraft mc = Minecraft.getInstance(); Vec3 center = box.getCenter(); double dx = center.x - mc.player.getX(); double dz = center.z - mc.player.getZ(); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/JumpCircles.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/JumpCircles.java index 2dced6f..0497f15 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/JumpCircles.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/JumpCircles.java @@ -40,7 +40,6 @@ import java.util.List; category = ModuleCategory.VISUALS ) public class JumpCircles extends Module { - private final Minecraft mc = Minecraft.getInstance(); private final EnumValue<CircleMode> mode = enumSetting("jumpCirclesMode", "mode", CircleMode.DEFAULT); private final BooleanValue easeOut = bool("jumpCirclesEaseOut", "ease_out", true); @@ -85,6 +84,7 @@ public class JumpCircles extends Module { @Override public void onTick() { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.level == null || mc.player == null) return; boolean selfOnly = onlySelf.get(); @@ -118,6 +118,7 @@ public class JumpCircles extends Module { @Override public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.level == null || mc.player == null || circles.isEmpty()) return; boolean useDepth = depthTest.get(); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/KillEffect.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/KillEffect.java index ae5b2ae..fef0d3f 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/KillEffect.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/KillEffect.java @@ -64,7 +64,6 @@ public class KillEffect extends Module { private static final Identifier ORTHODOX_SOUND = Identifier.fromNamespaceAndPath("lovisual", "sounds/misc/orthodox.wav"); private static final long EMBER_GROUND_EXTRA_MS = 0; - private final Minecraft mc = Minecraft.getInstance(); private final Random random = new Random(); private final ModeValue mode = modeSetting("killEffectMode", SETTING_MODE, "Orthodox", "Orthodox", "FallingLava", "EmbersArc", "Lightning"); @@ -149,6 +148,7 @@ public class KillEffect extends Module { @Override public void onTick() { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.level == null || mc.player == null) return; long now = GameClock.millis(); @@ -213,6 +213,7 @@ public class KillEffect extends Module { @Override public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.level == null || mc.player == null) return; if ("Orthodox".equals(mode.get())) { @@ -348,6 +349,7 @@ public class KillEffect extends Module { } private void spawnFallingLava(LivingEntity entity) { + Minecraft mc = Minecraft.getInstance(); if (mc.level == null) return; int height = (int) (entity.getBbHeight() * 10); int width = (int) (entity.getBbWidth() * 10); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/Light.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/Light.java new file mode 100644 index 0000000..6ee6561 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/Light.java @@ -0,0 +1,89 @@ +/* + * 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; + +import dev.loki.lovisual.config.values.BooleanValue; +import dev.loki.lovisual.config.values.NumberValue; +import dev.loki.lovisual.config.values.RGBColorValue; +import dev.loki.lovisual.events.EventHandler; +import dev.loki.lovisual.events.impl.LightmapModifyEvent; +import dev.loki.lovisual.features.module.Module; +import dev.loki.lovisual.features.module.ModuleCategory; +import dev.loki.lovisual.features.module.ModuleInfo; +import org.joml.Vector3f; +import org.joml.Vector3fc; + +/** + * Tints the world ambient light with a custom color. Pure math lives in + * {@link #tintAmbient} so it is unit-testable without Minecraft. + */ +@ModuleInfo( + id = "light", + displayName = "Light", + category = ModuleCategory.VISUALS +) +public class Light extends Module { + + private static final String SETTING_TINT_AMBIENT = "tint_ambient"; + private static final String SETTING_TINT_SKY = "tint_sky"; + private static final String SETTING_COLOR = "color"; + private static final String SETTING_STRENGTH = "strength"; + + private final BooleanValue tintAmbientSetting = bool("lightTintAmbient", SETTING_TINT_AMBIENT, true); + private final BooleanValue tintSkySetting = bool("lightTintSky", SETTING_TINT_SKY, false); + private final RGBColorValue color = colorNoAlpha("lightColor", SETTING_COLOR, "#C7D9FF"); + private final NumberValue<Integer> strength = num("lightStrength", SETTING_STRENGTH, 50, 0, 100); + + @EventHandler + private void onLightmapState(LightmapModifyEvent event) { + if (!isEnabled() || event.state() == null) return; + + float mix = clamp01(strength.get() / 100.0f); + if (mix <= 0.0f) return; + + int argb = color.getArgb(); + Vector3f target = toLinear(argb); + + var state = event.state(); + if (tintAmbientSetting.get()) { + state.ambientColor = tintAmbient(state.ambientColor, target, mix); + } + if (tintSkySetting.get()) { + state.skyLightColor = tintAmbient(state.skyLightColor, target, mix); + } + state.needsUpdate = true; + } + + /** + * @param current existing channel (may be null) + * @param target replacement linear RGB + * @param mix 0..1 blend factor; 0 keeps current, 1 is fully target + */ + public static Vector3f tintAmbient(Vector3fc current, Vector3f target, float mix) { + float k = clamp01(mix); + Vector3f base = current == null ? new Vector3f(1.0f, 1.0f, 1.0f) + : new Vector3f(current.x(), current.y(), current.z()); + base.x = base.x * (1.0f - k) + target.x * k; + base.y = base.y * (1.0f - k) + target.y * k; + base.z = base.z * (1.0f - k) + target.z * k; + return base; + } + + /** Converts 0xRRGGBB (alpha ignored) into a linear-ish 0..1 tint vector. */ + public static Vector3f toLinear(int argb) { + return new Vector3f( + ((argb >> 16) & 0xFF) / 255.0f, + ((argb >> 8) & 0xFF) / 255.0f, + (argb & 0xFF) / 255.0f + ); + } + + private static float clamp01(float v) { + return v < 0.0f ? 0.0f : Math.min(v, 1.0f); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/LightningTargetESP.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/LightningTargetESP.java index 99521ce..5f70353 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/LightningTargetESP.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/LightningTargetESP.java @@ -34,8 +34,6 @@ import java.util.concurrent.ThreadLocalRandom; ) public class LightningTargetESP extends Module { - private final Minecraft mc = Minecraft.getInstance(); - private final NumberValue<Float> lightningCount = num("lightningCount", "count", 4.0f, 1.0f, 10.0f); private final NumberValue<Float> lightningWidth = num("lightningWidth", "width", 1.0f, 0.1f, 3.0f); private final BooleanValue hurtColor = bool("hurtColor", "hurt_color", true); @@ -58,6 +56,7 @@ public class LightningTargetESP extends Module { @Override public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); if (mc.player == null || mc.level == null) return; LivingEntity currentTarget = TargetManager.getTarget(); @@ -87,6 +86,7 @@ public class LightningTargetESP extends Module { } private void renderLightning(Renderer3D renderer, float partialTicks, boolean hasActiveTarget, LivingEntity activeTarget) { + Minecraft mc = Minecraft.getInstance(); if (lastTargetPos == null) return; long now = System.currentTimeMillis(); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/MotionBlur.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/MotionBlur.java index ed52a69..fec1821 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/MotionBlur.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/MotionBlur.java @@ -36,7 +36,6 @@ public final class MotionBlur extends Module implements PostProcessPass { private static final float VERTICAL_DEGREES_PER_SCREEN = 70.0f; private static final int SHADER_TAPS = 11; - private final Minecraft mc = Minecraft.getInstance(); private final MotionBlurResources resources = new MotionBlurResources(); private final NumberValue<Float> strength = @@ -79,6 +78,7 @@ public final class MotionBlur extends Module implements PostProcessPass { @Override public boolean isActive() { + Minecraft mc = Minecraft.getInstance(); return isEnabled() && mc != null && mc.player != null && mc.level != null; } @@ -258,6 +258,7 @@ public final class MotionBlur extends Module implements PostProcessPass { } private boolean shouldReset(long nowNs) { + Minecraft mc = Minecraft.getInstance(); if (mc == null || mc.level == null || mc.options == null) return true; if (!previousFrameValid || previousFrameNs == 0L) return true; if (previousWorld != mc.level) return true; @@ -270,10 +271,12 @@ public final class MotionBlur extends Module implements PostProcessPass { } private CameraType currentPerspective() { + Minecraft mc = Minecraft.getInstance(); return mc != null && mc.options != null ? mc.options.getCameraType() : CameraType.FIRST_PERSON; } private void capturePose(float yaw, float pitch, long nowNs) { + Minecraft mc = Minecraft.getInstance(); previousYaw = yaw; previousPitch = pitch; previousWorld = mc != null ? mc.level : null; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/NameTagLabelBuilder.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/NameTagLabelBuilder.java index 81519ae..edf5570 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/NameTagLabelBuilder.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/NameTagLabelBuilder.java @@ -11,6 +11,7 @@ import dev.loki.lovisual.features.relations.PlayerRelations; import dev.loki.lovisual.util.player.PlayerHealthResolver; import dev.loki.lovisual.util.pvp.PvpTargetState; import dev.loki.lovisual.util.text.LegacyTextUtil; +import net.minecraft.client.Minecraft; import net.minecraft.client.multiplayer.PlayerInfo; import net.minecraft.network.chat.Component; import net.minecraft.util.Mth; @@ -45,11 +46,12 @@ final class NameTagLabelBuilder { } static Component resolveNameplateText(NameTags self, Player player) { + Minecraft mc = Minecraft.getInstance(); if (player == null) return Component.empty(); String fallback = player.getGameProfile().name(); if (!shouldUseTabNames(self)) return Component.literal(fallback); - var handler = self.mc.getConnection(); + var handler = mc.getConnection(); if (handler == null) return Component.literal(fallback); PlayerInfo entry = handler.getPlayerInfo(player.getUUID()); if (entry == null) return Component.literal(fallback); @@ -105,8 +107,9 @@ final class NameTagLabelBuilder { } static boolean isCreative(NameTags self, Player player) { + Minecraft mc = Minecraft.getInstance(); if (player.isCreative()) return true; - var handler = self.mc.getConnection(); + var handler = mc.getConnection(); if (handler == null) return false; PlayerInfo entry = handler.getPlayerInfo(player.getUUID()); if (entry == null) return false; @@ -123,8 +126,9 @@ final class NameTagLabelBuilder { } static int getPlayerPing(NameTags self, Player player) { - if (player == null || self.mc == null || self.mc.getConnection() == null) return -1; - PlayerInfo entry = self.mc.getConnection().getPlayerInfo(player.getUUID()); + Minecraft mc = Minecraft.getInstance(); + if (player == null || mc == null || mc.getConnection() == null) return -1; + PlayerInfo entry = mc.getConnection().getPlayerInfo(player.getUUID()); return entry != null ? entry.getLatency() : -1; } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/NameTagLayoutEngine.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/NameTagLayoutEngine.java index 078437c..1cfc878 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/NameTagLayoutEngine.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/NameTagLayoutEngine.java @@ -11,6 +11,7 @@ import dev.loki.lovisual.features.module.Modules; import dev.loki.lovisual.features.relations.CategoryService; import dev.loki.lovisual.render.engine.text.TextRenderer; import dev.loki.lovisual.util.text.TextRenderUtil; +import net.minecraft.client.Minecraft; import net.minecraft.network.chat.Component; import net.minecraft.util.Mth; import net.minecraft.world.entity.player.Player; @@ -150,9 +151,10 @@ final class NameTagLayoutEngine { } static boolean shouldShowDistanceText(NameTags self, Player player) { + Minecraft mc = Minecraft.getInstance(); if (!self.toggles.get("Show distance")) return false; - if (player == null || self.mc.player == null) return false; - if (player != self.mc.player) return true; + if (player == null || mc.player == null) return false; + if (player != mc.player) return true; Freecam freecam = Modules.get(Freecam.class); return freecam != null && freecam.isEnabled(); } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/NameTagOpponentRenderer.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/NameTagOpponentRenderer.java index 5565efb..e8fc41f 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/NameTagOpponentRenderer.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/NameTagOpponentRenderer.java @@ -16,6 +16,7 @@ import dev.loki.lovisual.util.pvp.CooldownRegistry; import dev.loki.lovisual.util.pvp.ItemCooldownSnapshot; import dev.loki.lovisual.util.pvp.PvpTargetState; import dev.loki.lovisual.util.pvp.opponents.OpponentCooldownManager; +import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.Item; @@ -37,13 +38,14 @@ final class NameTagOpponentRenderer { } static boolean shouldRenderOpponentStuff(NameTags self, Player p) { - if (p == null || self.mc.player == null) return false; - if (p == self.mc.player) return false; + Minecraft mc = Minecraft.getInstance(); + if (p == null || mc.player == null) return false; + if (p == mc.player) return false; if (!self.opponentAppleCooldowns.get()) return false; if (!isOpponentAppleTrackingEnabled()) return false; if (!PvpTargetState.isTargetInPvp(p.getUUID()) && !hasOpponentCooldowns(p.getUUID())) return false; double maxDist = NameTags.OPPONENT_DISTANCE; - return !(maxDist > 0) || !(self.mc.player.distanceToSqr(p) > maxDist * maxDist); + return !(maxDist > 0) || !(mc.player.distanceToSqr(p) > maxDist * maxDist); } static boolean isOpponentAppleTrackingEnabled() { diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/NameTags.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/NameTags.java index bbb81df..5de5d73 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/NameTags.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/NameTags.java @@ -73,7 +73,6 @@ public class NameTags extends Module { private static final String SETTING_PVP_PREFIX = "pvp_prefix"; private static final String SETTING_PVP_PREFIX_COLOR = "pvp_prefix_color"; - final Minecraft mc = Minecraft.getInstance(); private final ItemIdSetValue topIgnore = TopEnchantUtil.ignoreValue(); private final List<NameTagModel.RenderEntry> renderQueue = new ArrayList<>(); private final List<Player> playerBuffer = new ArrayList<>(); @@ -224,6 +223,7 @@ public class NameTags extends Module { @Override public void onRenderHudEngine(Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.level == null || mc.player == null) return; NameTagRenderer.ensureFontCache(this, textRenderer); @@ -373,6 +373,7 @@ public class NameTags extends Module { @Override public void onRenderHudEngineForeground(Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.level == null || mc.player == null) return; NameTagRenderer.ensureFontCache(this, textRenderer); @@ -383,6 +384,7 @@ public class NameTags extends Module { } private boolean shouldRender(Player player) { + Minecraft mc = Minecraft.getInstance(); if (player == null || mc.player == null) return false; if (player == mc.player) { CameraType p = mc.options.getCameraType(); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/PostFX.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/PostFX.java index 3868c6c..ac9256a 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/PostFX.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/PostFX.java @@ -51,7 +51,6 @@ public class PostFX extends Module implements PostProcessPass { private static final String SETTING_MID_STRENGTH = "mid_strength"; private static final String SETTING_HIGH_TINT = "high_tint"; private static final String SETTING_HIGH_STRENGTH = "high_strength"; - private final Minecraft mc = Minecraft.getInstance(); private final ModeValue preset = modeSetting("postFxPreset", SETTING_PRESET, "Cinematic", "Cinematic", "Vanilla", "Soft", "Warm", "Cool", "Custom"); @@ -117,6 +116,7 @@ public class PostFX extends Module implements PostProcessPass { @Override public boolean isActive() { + Minecraft mc = Minecraft.getInstance(); return isEnabled() && mc != null && mc.player != null && mc.level != null; } @@ -132,6 +132,7 @@ public class PostFX extends Module implements PostProcessPass { @Override public boolean render(GpuTextureView src, GpuTextureView dst, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); if (!isActive()) return false; if (src == null || dst == null) return false; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/Rain.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/Rain.java index 24587cb..09c173f 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/Rain.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/Rain.java @@ -63,8 +63,6 @@ public final class Rain extends Module implements PostProcessPass { private static final int MAX_DROPS = 4200; private static final int MAX_MIST = 90; - private final Minecraft mc = Minecraft.getInstance(); - private final EnumValue<Preset> preset = enumMode("preset", Preset.RAIN, Preset.values()); private final NumberValue<Float> density = num("density", 1.0f, 0.1f, 3.0f); private final NumberValue<Float> radius = num("radius", 26.0f, 8.0f, 64.0f); @@ -130,6 +128,7 @@ public final class Rain extends Module implements PostProcessPass { @Override public void onTick() { + Minecraft mc = Minecraft.getInstance(); if (mc.player == null || mc.level == null) { clearAll(); return; @@ -187,6 +186,7 @@ public final class Rain extends Module implements PostProcessPass { } private int surfaceY(int x, int z) { + Minecraft mc = Minecraft.getInstance(); long key = (((long) x) << 32) ^ (z & 0xFFFFFFFFL); Integer cached = groundCache.get(key); if (cached != null) return cached; @@ -202,6 +202,7 @@ public final class Rain extends Module implements PostProcessPass { } private boolean isBlocking(double x, double y, double z) { + Minecraft mc = Minecraft.getInstance(); if (mc.level == null) return false; scratchPos.set(Mth.floor(x), Mth.floor(y), Mth.floor(z)); @@ -216,6 +217,7 @@ public final class Rain extends Module implements PostProcessPass { } private boolean isWater(double x, double y, double z) { + Minecraft mc = Minecraft.getInstance(); if (mc.level == null) return false; scratchPos.set(Mth.floor(x), Mth.floor(y), Mth.floor(z)); try { @@ -226,6 +228,7 @@ public final class Rain extends Module implements PostProcessPass { } private boolean skyVisible(double x, double y, double z) { + Minecraft mc = Minecraft.getInstance(); if (!skyCheck.get() || mc.level == null) return true; scratchPos.set(Mth.floor(x), Mth.floor(y), Mth.floor(z)); try { @@ -426,6 +429,7 @@ public final class Rain extends Module implements PostProcessPass { } private float ambientLight(Camera camera) { + Minecraft mc = Minecraft.getInstance(); try { int light = mc.level.getBrightness(net.minecraft.world.level.LightLayer.BLOCK, camera.blockPosition()); return Mth.clamp(light / 15.0f, 0.3f, 1.0f); @@ -450,6 +454,7 @@ public final class Rain extends Module implements PostProcessPass { @Override public boolean isActive() { + Minecraft mc = Minecraft.getInstance(); return isEnabled() && wetGround.get() && wetSurfaceDepthSupported @@ -528,6 +533,7 @@ public final class Rain extends Module implements PostProcessPass { @Override public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.player == null || mc.level == null || mc.gameRenderer == null) return; if (mc.gameRenderer.mainCamera() == null) return; if (onlyWhenRaining.get() && !mc.level.isRaining()) { diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/ReimaginedVisual.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/ReimaginedVisual.java index f5aaacd..c15811f 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/ReimaginedVisual.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/ReimaginedVisual.java @@ -148,7 +148,6 @@ public class ReimaginedVisual extends Module implements PostProcessPass { visibleWhen(num("reimaginedVisualWavyVegetationFreeVerticalAmplitude", SETTING_WAVY_VEGETATION_FREE_VERTICAL_AMPLITUDE, 1.0f, 0.0f, 3.0f), this::isWavyVegetationSettingsVisible); private final NumberValue<Float> wavyVegetationSpeed = visibleWhen(num("reimaginedVisualWavyVegetationSpeed", SETTING_WAVY_VEGETATION_SPEED, 1.0f, 0.0f, 3.0f), this::isWavyVegetationSettingsVisible); - private final Minecraft mc = Minecraft.getInstance(); private boolean wavyVegetationStateInitialized; private boolean lastWavyVegetationActive; private int lastWavyVegetationSettingsPacked; @@ -332,6 +331,7 @@ public class ReimaginedVisual extends Module implements PostProcessPass { @Override public boolean isActive() { + Minecraft mc = Minecraft.getInstance(); return isEnabled() && isEffectSelected(EFFECT_DEPTH_OF_FIELD) && mc != null diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/ShaderSky.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/ShaderSky.java index c2e8af1..c0e7043 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/ShaderSky.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/ShaderSky.java @@ -14,8 +14,6 @@ import net.minecraft.client.Minecraft; @ModuleInfo(id = "shadersky", displayName = "ShaderSky", category = ModuleCategory.VISUALS) public class ShaderSky extends Module { - private final Minecraft mc = Minecraft.getInstance(); - private static final String SETTING_MODE = "mode"; private static final String SETTING_SPEED = "speed"; private static final String SETTING_SCALE = "scale"; @@ -99,6 +97,7 @@ public class ShaderSky extends Module { @Override public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.level == null || mc.player == null) return; if (isSkyShaderMode()) { diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/SoundESP.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/SoundESP.java index c5a2a05..054759f 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/SoundESP.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/SoundESP.java @@ -43,7 +43,6 @@ public class SoundESP extends Module { private static final double TEXT_SCALE = 0.82; private static final long FADE_IN_MS = 160L; private static final long FADE_OUT_MS = 360L; - private final Minecraft mc = Minecraft.getInstance(); private final ModeValue nameMode = modeSetting( "soundEspNameMode", SETTING_NAME_MODE, @@ -85,6 +84,7 @@ public class SoundESP extends Module { } public void handleSound(Identifier id, Vec3 pos) { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.level == null || mc.player == null) return; if (id == null || pos == null) return; synchronized (soundLock) { @@ -101,6 +101,7 @@ public class SoundESP extends Module { @Override public void onRenderHudEngine(Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.level == null || mc.player == null) return; long now = GameClock.millis(); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/TazikHat.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/TazikHat.java index 8abc3a0..fcf0c5d 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/TazikHat.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/TazikHat.java @@ -55,7 +55,6 @@ public final class TazikHat extends Module { 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 Minecraft mc = Minecraft.getInstance(); private final Random random = new Random(); private final EnumValue<RenderTarget> renderTarget = enumMode( @@ -301,6 +300,7 @@ public final class TazikHat extends Module { @Override public void onTick() { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.level == null || mc.player == null) { return; } @@ -343,6 +343,7 @@ public final class TazikHat extends Module { @Override public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.level == null || mc.player == null) return; MeshBuilder tris = renderer.batch( @@ -589,6 +590,7 @@ public final class TazikHat extends Module { } private void spawnFlyingTazik(Player player) { + Minecraft mc = Minecraft.getInstance(); if (player == null) return; boolean helmet = hasHelmet(player); @@ -799,6 +801,7 @@ public final class TazikHat extends Module { } private boolean update() { + Minecraft mc = Minecraft.getInstance(); long age = GameClock.millis() - createdAtMs; if (age > DEATH_LIFE_MS) { return true; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/TotemFX.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/TotemFX.java index 1536741..86446da 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/TotemFX.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/TotemFX.java @@ -38,7 +38,6 @@ public class TotemFX extends Module implements PostProcessPass { private static final String SETTING_FADE_OUT = "fade_out"; private static final String SETTING_PULSE_SPEED = "pulse_speed"; private static final String SETTING_PULSE_AMOUNT = "pulse_amount"; - private final Minecraft mc = Minecraft.getInstance(); private final NumberValue<Float> intensity = num("totemFxIntensity", SETTING_INTENSITY, 0.65f, 0.0f, 2.0f); private final NumberValue<Float> distortion = @@ -77,6 +76,7 @@ public class TotemFX extends Module implements PostProcessPass { @Override public boolean isActive() { + Minecraft mc = Minecraft.getInstance(); return isEnabled() && mc.player != null && mc.level != null @@ -95,6 +95,7 @@ public class TotemFX extends Module implements PostProcessPass { @Override public boolean render(GpuTextureView src, GpuTextureView dst, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.player == null || mc.level == null) return false; if (src == null || dst == null) return false; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/Trails.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/Trails.java index 8c0c4c4..7605a11 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/Trails.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/Trails.java @@ -77,7 +77,6 @@ public class Trails extends Module { private static final int PROJECTILE_SCAN_RADIUS = 48; private static final int PROJECTILE_MAX_SPAWN = 140; private static final int CONNECT_MAX_STEPS = 6; - private final Minecraft mc = Minecraft.getInstance(); private final Random random = new Random(); private final BooleanValue onlySelf = bool("trailsOnlySelf", SETTING_ONLY_SELF, false); private final BooleanValue hideFirstPerson = bool("trailsHideFirstPerson", SETTING_HIDE_FIRST_PERSON, true); @@ -235,6 +234,7 @@ public class Trails extends Module { @Override public void onTick() { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.level == null || mc.player == null) return; int colorIndex = mc.player.tickCount % 360; @@ -298,6 +298,7 @@ public class Trails extends Module { @Override public void onDisable() { + Minecraft mc = Minecraft.getInstance(); if (mc.level == null) return; for (Player player : mc.level.players()) { ((IEntity) player).lovisual$getTrails().clear(); @@ -308,6 +309,7 @@ public class Trails extends Module { @Override public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.level == null || mc.player == null) return; boolean useDepth = depthTest.get(); @@ -380,6 +382,7 @@ public class Trails extends Module { } private void renderRibbonTrails(Renderer3D renderer, float tickDelta, boolean useDepth) { + Minecraft mc = Minecraft.getInstance(); RenderPipeline pipeline = useDepth ? LoVisualRenderPipelines.WORLD_COLORED_LIQUID_IGNORE : LoVisualRenderPipelines.WORLD_COLORED; @@ -462,6 +465,7 @@ public class Trails extends Module { } private void spawnProjectileParticles(int colorIndex) { + Minecraft mc = Minecraft.getInstance(); if (mc.player == null) return; int count = particleProjectileCount.get(); if (count <= 0) return; @@ -632,6 +636,7 @@ public class Trails extends Module { } private int currentColorPhase(float tickDelta) { + Minecraft mc = Minecraft.getInstance(); if (mc.player == null) return colorPhase.get(); return Math.round((mc.player.tickCount + tickDelta) * 4.0f) + colorPhase.get(); } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/ViewModel.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/ViewModel.java index 094fece..ce3bbcd 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/ViewModel.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/ViewModel.java @@ -52,7 +52,6 @@ public class ViewModel extends Module { private static final String SETTING_EQUIP_LOWERING = "equip_lowering_factor"; private static final String SETTING_SMOOTH_RUN_LOWERING = "smooth_run_lowering"; private static final String SETTING_SMOOTH_RUN_LOWERING_AMOUNT = "smooth_run_lowering_amount"; - public final Minecraft mc = Minecraft.getInstance(); public final NumberValue<Float> liquidOffsetZ = num("liquid_offset_z", SETTING_LIQUID_OFFSET_Z, 0.0f, -2.0f, 2.0f); /* ----------------------------- * MINI ITEMS @@ -148,6 +147,7 @@ public class ViewModel extends Module { * -------------------------------------------- */ public void applySwingAnimation(float swingProg, PoseStack ms, HumanoidArm arm) { + Minecraft mc = Minecraft.getInstance(); if (arm == HumanoidArm.LEFT) return; if (mc.player == null) return; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/WorldParticles.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/WorldParticles.java index b962bae..aa9f927 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/WorldParticles.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/WorldParticles.java @@ -84,7 +84,6 @@ public class WorldParticles extends Module { {0, 6}, {1, 7}, {2, 4}, {3, 5} }; - private final Minecraft mc = Minecraft.getInstance(); private final EnumValue<Mode> mode = enumSetting("mode", "mode", Mode.QUADS); private final BooleanValue depthTest = @@ -358,6 +357,7 @@ public class WorldParticles extends Module { @Override public void onTick() { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.player == null || mc.level == null) { return; } @@ -384,6 +384,7 @@ public class WorldParticles extends Module { @Override public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || particles.isEmpty() || mc.player == null || mc.level == null) { return; } @@ -732,6 +733,7 @@ public class WorldParticles extends Module { } private Particle createParticle() { + Minecraft mc = Minecraft.getInstance(); ThreadLocalRandom random = ThreadLocalRandom.current(); Vec3 playerPos = mc.player.position(); 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 eee4d4e..6f8052e 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 @@ -35,8 +35,6 @@ import java.util.concurrent.ThreadLocalRandom; ) public final class KineticLyrics extends Module { - private final Minecraft mc = Minecraft.getInstance(); - private final EnumValue<AnimationMode> animation = enumMode("animation", AnimationMode.LYRIC_FLOW, AnimationMode.values()); private final EnumValue<ColorMode> colorMode = @@ -97,6 +95,7 @@ public final class KineticLyrics extends Module { @Override public void onTick() { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.player == null) return; syncPlaybackTime(); } @@ -192,6 +191,7 @@ public final class KineticLyrics extends Module { } private void advanceQueue(long audioTimeMs) { + Minecraft mc = Minecraft.getInstance(); if (!isPlaying || lyricsQueue.isEmpty() || mc.player == null) return; long now = System.currentTimeMillis(); @@ -265,6 +265,7 @@ public final class KineticLyrics extends Module { } private void spawnLyricParticle(String text, long spawnTimeMs, long durationMs) { + Minecraft mc = Minecraft.getInstance(); if (mc.player == null || text == null || text.isBlank()) return; if (mc.gameRenderer == null || mc.gameRenderer.mainCamera() == null) return; @@ -320,6 +321,7 @@ public final class KineticLyrics extends Module { @Override public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || mc.level == null || mc.player == null) return; if (activeParticles.isEmpty()) return; @@ -335,6 +337,7 @@ public final class KineticLyrics extends Module { @Override public void onRenderHudEngineForeground(Renderer2D renderer, TextRenderer textRenderer, GuiGraphicsExtractor ctx, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); if (!isEnabled() || !debugMode.get() || mc.player == null) return; TextRenderer tr = Fonts.renderer("OnestMedium", FontInfo.Type.Regular, textRenderer); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/trollface/TrollfaceMask.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/trollface/TrollfaceMask.java index 2b4dd11..9e2e30f 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/trollface/TrollfaceMask.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/features/module/modules/visuals/trollface/TrollfaceMask.java @@ -30,7 +30,6 @@ import net.minecraft.world.phys.Vec3; public class TrollfaceMask extends Module { private static TrollfaceMask instance; - private final Minecraft mc = Minecraft.getInstance(); public final ModeValue target = mode( "trollfaceTarget", "Себя", @@ -61,6 +60,7 @@ public class TrollfaceMask extends Module { @Override public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); if (mc.player == null || mc.level == null) return; String targetMode = target.get(); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/AbstractClientPlayerMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/AbstractClientPlayerMixin.java index d6d4559..123487d 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/AbstractClientPlayerMixin.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/AbstractClientPlayerMixin.java @@ -9,11 +9,13 @@ package dev.loki.lovisual.mixins; import net.minecraft.client.Minecraft; import net.minecraft.client.player.AbstractClientPlayer; +import net.minecraft.client.resources.DefaultPlayerSkin; import net.minecraft.util.Mth; import net.minecraft.world.effect.MobEffects; import net.minecraft.world.entity.ai.attributes.Attributes; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; +import net.minecraft.world.entity.player.PlayerSkin; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; @@ -22,6 +24,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import dev.loki.lovisual.features.module.Modules; import dev.loki.lovisual.features.module.modules.combat.PvpCooldowns; +import dev.loki.lovisual.features.module.modules.misc.StreamerMode; import dev.loki.lovisual.features.module.modules.visuals.FovControl; import dev.loki.lovisual.util.pvp.CooldownRegistry; import dev.loki.lovisual.util.pvp.PvpTargetState; @@ -88,6 +91,18 @@ public class AbstractClientPlayerMixin { cir.setReturnValue(mult); } + @Inject(method = "getSkin", at = @At("RETURN"), cancellable = true) + private void lovisual$streamerSkin(CallbackInfoReturnable<PlayerSkin> cir) { + StreamerMode module = Modules.get(StreamerMode.class); + if (module == null || !module.shouldHideSkins()) return; + + AbstractClientPlayer player = (AbstractClientPlayer) (Object) this; + Minecraft mc = Minecraft.getInstance(); + if (mc.player != null && player.getUUID().equals(mc.player.getUUID())) return; + + cir.setReturnValue(DefaultPlayerSkin.get(player.getUUID())); + } + @Inject(method = "tick", at = @At("TAIL")) private void lovisual$trackEat(CallbackInfo ci) { AbstractClientPlayer p = (AbstractClientPlayer) (Object) this; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/AbstractContainerScreenMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/AbstractContainerScreenMixin.java index ffe1fd3..fd5820f 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/AbstractContainerScreenMixin.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/AbstractContainerScreenMixin.java @@ -8,6 +8,7 @@ package dev.loki.lovisual.mixins; import dev.loki.lovisual.features.module.Modules; +import dev.loki.lovisual.features.module.modules.misc.ItemScroller; import dev.loki.lovisual.features.module.modules.visuals.Animations; import dev.loki.lovisual.features.module.modules.visuals.ShulkerPreview; import net.minecraft.client.gui.GuiGraphicsExtractor; @@ -21,6 +22,7 @@ import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; @Mixin(AbstractContainerScreen.class) public abstract class AbstractContainerScreenMixin { @@ -98,4 +100,22 @@ public abstract class AbstractContainerScreenMixin { if (stack.isEmpty()) return; ShulkerPreview.onScreenExtract(graphics, stack, mouseX, mouseY); } + + @Inject( + method = "mouseScrolled(DDDD)Z", + at = @At("HEAD"), + cancellable = true + ) + private void lovisual$itemScroller$quickMove(double mouseX, double mouseY, + double horizontal, double vertical, + CallbackInfoReturnable<Boolean> cir) { + ItemScroller scroller = Modules.get(ItemScroller.class); + if (scroller == null || !scroller.isEnabled()) return; + + Slot slot = this.hoveredSlot; + ItemStack stack = slot == null ? null : slot.getItem(); + if (scroller.onMouseScrolled(vertical, slot, stack)) { + cir.setReturnValue(true); + } + } } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/CameraMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/CameraMixin.java index 56cd391..f883b82 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/CameraMixin.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/CameraMixin.java @@ -28,6 +28,7 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import dev.loki.lovisual.features.module.Modules; +import dev.loki.lovisual.features.module.modules.player.ThirdPerson; import dev.loki.lovisual.features.module.modules.visuals.CameraClip; import dev.loki.lovisual.features.module.modules.visuals.FovControl; import dev.loki.lovisual.features.module.modules.visuals.Freecam; @@ -138,6 +139,47 @@ public abstract class CameraMixin { return p == CameraType.THIRD_PERSON_BACK || p == CameraType.THIRD_PERSON_FRONT; } + @Unique + private static float thirdPerson$turnRad(float deg) { + return (float) Math.toRadians(Mth.wrapDegrees(deg)); + } + + @Inject(method = "update", at = @At("TAIL")) + private void thirdPerson$orbit(DeltaTracker tickCounter, CallbackInfo ci) { + ThirdPerson mod = Modules.get(ThirdPerson.class); + if (mod == null || !mod.isLooking()) { + if (mod != null) { + // Orbits decay to the player's own view so re-entering looks natural. + mod.orbitYaw = 0.0f; + mod.orbitPitch = 0.0f; + } + return; + } + Minecraft mc = Minecraft.getInstance(); + if (mc == null || mc.player == null || !isThirdPerson()) return; + + float yaw = mc.player.getYRot() + thirdPerson$turnRad(mod.orbitYaw); + float pitch = Mth.clamp(mod.orbitPitch, -90.0f, 90.0f); + + double dist = mod.distance(); + double yawRad = Math.toRadians(yaw); + double pitchRad = Math.toRadians(pitch); + + double dx = -Math.sin(yawRad) * Math.cos(pitchRad) * dist; + double dy = -Math.sin(pitchRad) * dist; + double dz = Math.cos(yawRad) * Math.cos(pitchRad) * dist; + + Vec3 eye = mc.player.getEyePosition(); + Vec3 target = new Vec3(eye.x + dx, eye.y + dy, eye.z + dz); + this.setRotation(yaw, pitch); + this.setPosition(target); + + this.lovisual$prepareCullFrustum( + this.getViewRotationMatrix(new Matrix4f()), + this.lovisual$createProjectionMatrixForCulling(), + target); + } + @Shadow protected abstract void setRotation(float yaw, float pitch); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/ChatComponentMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/ChatComponentMixin.java index f08c8bb..85f03ff 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/ChatComponentMixin.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/ChatComponentMixin.java @@ -19,17 +19,31 @@ import net.minecraft.network.chat.MessageSignature; 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.ModifyVariable; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import dev.loki.lovisual.features.command.CommandOutput; import dev.loki.lovisual.features.gui.chat.BetterChatStoreManager; import dev.loki.lovisual.features.gui.hud.draggable.impl.BetterChat; import dev.loki.lovisual.features.module.Modules; import dev.loki.lovisual.features.module.modules.misc.MessageFilter; +import dev.loki.lovisual.features.module.modules.misc.NameProtect; +import dev.loki.lovisual.features.module.modules.misc.StreamerMode; import dev.loki.lovisual.Lifecycle; @Mixin(ChatComponent.class) public class ChatComponentMixin { + @ModifyVariable( + method = "addMessage(Lnet/minecraft/network/chat/Component;Lnet/minecraft/network/chat/MessageSignature;Lnet/minecraft/client/multiplayer/chat/GuiMessageSource;Lnet/minecraft/client/multiplayer/chat/GuiMessageTag;)V", + at = @At("HEAD"), + argsOnly = true + ) + private Component lovisual$nameProtectChat(Component message) { + if (!Lifecycle.canRunHud()) return message; + Component protectedMessage = NameProtect.protect(message); + return StreamerMode.protectComponent(protectedMessage); + } + @Inject( method = "addMessage(Lnet/minecraft/network/chat/Component;Lnet/minecraft/network/chat/MessageSignature;Lnet/minecraft/client/multiplayer/chat/GuiMessageSource;Lnet/minecraft/client/multiplayer/chat/GuiMessageTag;)V", at = @At("HEAD"), diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/ItemInHandRendererMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/ItemInHandRendererMixin.java index 6c0e50a..df57fae 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/ItemInHandRendererMixin.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/ItemInHandRendererMixin.java @@ -27,6 +27,7 @@ import org.spongepowered.asm.mixin.injection.ModifyVariable; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import dev.loki.lovisual.features.module.Modules; import dev.loki.lovisual.features.module.modules.visuals.HoldMyItems; +import dev.loki.lovisual.util.animation.hand.HandDynamics; import dev.loki.lovisual.features.module.modules.visuals.ViewModel; import dev.loki.lovisual.util.animation.EquipProgressComposer; import dev.loki.lovisual.util.animation.hand.HandAnimState; @@ -174,9 +175,10 @@ public abstract class ItemInHandRendererMixin { ) { ViewModel viewModel = Modules.get(ViewModel.class); if (viewModel == null || !viewModel.isEnabled()) return; - if (viewModel.mc.player == null) return; + Minecraft mc = Minecraft.getInstance(); + if (mc.player == null) return; - ItemStack stack = viewModel.mc.player.getMainHandItem(); + ItemStack stack = mc.player.getMainHandItem(); if (!viewModel.shouldSwing(stack)) return; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/MouseMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/MouseMixin.java index d6aae2e..b80b721 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/MouseMixin.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/MouseMixin.java @@ -33,6 +33,7 @@ import dev.loki.lovisual.features.module.ModuleManager; import dev.loki.lovisual.features.module.Modules; import dev.loki.lovisual.features.module.modules.visuals.Freecam; import dev.loki.lovisual.features.module.modules.visuals.Zoom; +import dev.loki.lovisual.features.module.modules.player.ThirdPerson; import dev.loki.lovisual.Lifecycle; import dev.loki.lovisual.util.input.KeyManager; import dev.loki.lovisual.util.logging.DebugLog; @@ -178,11 +179,13 @@ public class MouseMixin { Freecam fc = Modules.get(Freecam.class); if (!Lifecycle.isActive() || fc == null || !fc.isEnabled()) { + if (thirdPerson$orbitLook(dx, dy)) return; player.turn(dx, dy); return; } if (!fc.isCameraInput()) { + if (thirdPerson$orbitLook(dx, dy)) return; player.turn(dx, dy); return; } @@ -202,6 +205,22 @@ public class MouseMixin { fc.camPitch = Mth.clamp(fc.camPitch, -90.0F, 90.0F); } + @Unique + private boolean thirdPerson$orbitLook(double dx, double dy) { + ThirdPerson thirdPerson = Modules.get(ThirdPerson.class); + if (thirdPerson == null || !thirdPerson.isLooking()) return false; + + Minecraft mc = Minecraft.getInstance(); + double sens = mc.options.sensitivity().get(); + double f = sens * 0.6 + 0.2; + double scale = f * f * f * 8.0; + + thirdPerson.orbitYaw += (float) (dx * scale * 0.15); + thirdPerson.orbitPitch = ThirdPerson.clampPitch( + thirdPerson.orbitPitch + (float) (dy * scale * 0.15)); + return true; + } + @ModifyExpressionValue( method = "turnPlayer", at = @At( diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/MultiPlayerGameModeMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/MultiPlayerGameModeMixin.java index 4bb1cc1..ed294f1 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/MultiPlayerGameModeMixin.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/MultiPlayerGameModeMixin.java @@ -37,6 +37,7 @@ import dev.loki.lovisual.features.module.modules.visuals.Freecam; import dev.loki.lovisual.features.module.modules.visuals.HitEffect; import dev.loki.lovisual.mixins.accessors.WorldAccessor; import dev.loki.lovisual.util.player.inventory.InventorySwap; +import dev.loki.lovisual.util.combat.ReachMeter; import dev.loki.lovisual.util.target.TargetManager; @Mixin(MultiPlayerGameMode.class) @@ -168,6 +169,11 @@ public class MultiPlayerGameModeMixin { TargetHud.notifyHit(target); TargetManager.onAttack(target); + + double reachDistance = ReachMeter.distanceToBox(player.getEyePosition(), target.getBoundingBox()); + if (reachDistance >= 0.0) { + ReachMeter.record(reachDistance, System.currentTimeMillis()); + } } } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/PlayerInfoMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/PlayerInfoMixin.java index 4def8d3..f3e0e61 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/PlayerInfoMixin.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/PlayerInfoMixin.java @@ -22,6 +22,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import dev.loki.lovisual.features.gui.hud.nondraggable.impl.tab.CustomTabList; import dev.loki.lovisual.features.module.Modules; import dev.loki.lovisual.features.module.modules.misc.BetterMinecraft; +import dev.loki.lovisual.features.module.modules.misc.NameProtect; import dev.loki.lovisual.features.relations.CategoryService; import dev.loki.lovisual.features.relations.CategoryType; @@ -31,6 +32,17 @@ public abstract class PlayerInfoMixin { @Shadow public abstract GameProfile getProfile(); + @Inject(method = "getTabListDisplayName", at = @At("RETURN"), cancellable = true) + private void lovisual$nameProtectTab(CallbackInfoReturnable<Component> cir) { + Component original = cir.getReturnValue(); + if (original == null) return; + + Component protectedName = NameProtect.protect(original); + if (protectedName != original) { + cir.setReturnValue(protectedName); + } + } + @Inject(method = "getTabListDisplayName", at = @At("RETURN"), cancellable = true) private void injectColorIntoTab(CallbackInfoReturnable<Component> cir) { if (CustomTabList.shouldReplaceVanilla()) return; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/ScreenMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/ScreenMixin.java index 0b5ec15..c6cae29 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/ScreenMixin.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/ScreenMixin.java @@ -21,6 +21,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import dev.loki.lovisual.config.MainConfig; import dev.loki.lovisual.features.command.CommandManager; +import dev.loki.lovisual.features.command.CommandPrefix; import dev.loki.lovisual.features.gui.hud.nondraggable.impl.BetterButtons; import dev.loki.lovisual.features.module.Modules; import dev.loki.lovisual.features.module.modules.visuals.NoRender; @@ -42,7 +43,7 @@ public abstract class ScreenMixin { CallbackInfo ci) { if (!(event instanceof ClickEvent.RunCommand run)) return; String command = run.command(); - if (command == null || !command.startsWith("@")) return; + if (command == null || !CommandPrefix.isCommandLike(command)) return; if (CommandManager.handle(command)) { ci.cancel(); } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/SoundEngineMixin.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/SoundEngineMixin.java new file mode 100644 index 0000000..60d02e1 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/SoundEngineMixin.java @@ -0,0 +1,45 @@ +/* + * 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; + +import dev.loki.lovisual.features.module.Modules; +import dev.loki.lovisual.features.module.modules.misc.NoSound; +import net.minecraft.client.resources.sounds.Sound; +import net.minecraft.client.resources.sounds.SoundInstance; +import net.minecraft.client.sounds.SoundEngine; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +/** + * SoundReducer support: scales the resolved volume of listed sounds in + * NoSound's reduce mode. Mute mode keeps using the existing packet-level + * filter in ClientPacketListenerMixin. + */ +@Mixin(SoundEngine.class) +public abstract class SoundEngineMixin { + + @Redirect( + method = "play(Lnet/minecraft/client/resources/sounds/SoundInstance;)Lnet/minecraft/client/sounds/SoundEngine$PlayResult;", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/resources/sounds/SoundInstance;getVolume()F" + ) + ) + private float lovisual$scaleSoundVolume(SoundInstance instance) { + float original = instance.getVolume(); + + Sound resolved = instance.getSound(); + if (resolved == null) return original; + + NoSound module = Modules.get(NoSound.class); + if (module == null) return original; + + return original * module.volumeScale(resolved.getLocation().toString()); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/effects/NetherPortalRiftPass.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/effects/NetherPortalRiftPass.java index 0a140e5..34c3224 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/effects/NetherPortalRiftPass.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/effects/NetherPortalRiftPass.java @@ -23,12 +23,12 @@ public final class NetherPortalRiftPass implements PostProcessPass { private static final float FADE_IN_SECONDS = 0.25f; private static final float FADE_OUT_SECONDS = 0.35f; - private final Minecraft mc = Minecraft.getInstance(); private float strength; private long lastUpdateMs; @Override public boolean isActive() { + Minecraft mc = Minecraft.getInstance(); return mc != null && mc.player != null && mc.level != null; } @@ -44,6 +44,7 @@ public final class NetherPortalRiftPass implements PostProcessPass { @Override public boolean render(GpuTextureView src, GpuTextureView dst, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); if (mc == null || mc.player == null || mc.level == null) return false; if (src == null || dst == null) return false; @@ -99,6 +100,7 @@ public final class NetherPortalRiftPass implements PostProcessPass { } private boolean isInNetherPortalBlock() { + Minecraft mc = Minecraft.getInstance(); if (mc == null || mc.level == null || mc.player == null) return false; var pos = mc.player.blockPosition(); if (mc.level.getBlockState(pos).is(Blocks.NETHER_PORTAL)) return true; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/effects/SleepOverlayPass.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/effects/SleepOverlayPass.java index 0e0686f..3203f5e 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/effects/SleepOverlayPass.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/render/effects/SleepOverlayPass.java @@ -20,12 +20,12 @@ public final class SleepOverlayPass implements PostProcessPass { private static final float FADE_IN_SECONDS = 0.6f; private static final float FADE_OUT_SECONDS = 0.6f; - private final Minecraft mc = Minecraft.getInstance(); private float strength; private long lastUpdateMs; @Override public boolean isActive() { + Minecraft mc = Minecraft.getInstance(); return mc != null && mc.player != null && mc.level != null; } @@ -41,6 +41,7 @@ public final class SleepOverlayPass implements PostProcessPass { @Override public boolean render(GpuTextureView src, GpuTextureView dst, float tickDelta) { + Minecraft mc = Minecraft.getInstance(); if (mc == null || mc.player == null || mc.level == null) return false; if (src == null || dst == null) return false; diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/HandDynamics.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/HandDynamics.java similarity index 91% rename from combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/HandDynamics.java rename to combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/HandDynamics.java index eb8f275..369df10 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/mixins/HandDynamics.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/animation/hand/HandDynamics.java @@ -5,7 +5,7 @@ * Licensed under the GNU General Public License v3.0. */ -package dev.loki.lovisual.mixins; +package dev.loki.lovisual.util.animation.hand; import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.math.Axis; @@ -15,9 +15,6 @@ import net.minecraft.world.InteractionHand; import net.minecraft.world.entity.HumanoidArm; import dev.loki.lovisual.features.module.modules.visuals.HoldMyItems; import dev.loki.lovisual.render.helpers.TickDelta; -import dev.loki.lovisual.util.animation.hand.BobMath; -import dev.loki.lovisual.util.animation.hand.HandAnimState; -import dev.loki.lovisual.util.animation.hand.HandPoseMath; /** * Клей между рендером руки и чистой математикой динамики: собирает ввод @@ -25,7 +22,7 @@ import dev.loki.lovisual.util.animation.hand.HandPoseMath; * применяет её к матрице вокруг ванильных трансформов. Состояние не хранит — * оно живёт в {@link HandAnimState}, обновляемом раз за кадр. */ -final class HandDynamics { +public final class HandDynamics { /** Смещения/углы слабее этого считаются нулевыми и не применяются. */ private static final float SIGNIFICANT_EPSILON = 1.0e-4f; @@ -34,7 +31,7 @@ final class HandDynamics { } /** Один шаг динамики за кадр; вызывается из начала submitHandsWithItems. */ - static void beginFrame(HandAnimState state, AbstractClientPlayer player, float tickDelta) { + public static void beginFrame(HandAnimState state, AbstractClientPlayer player, float tickDelta) { state.beginFrame( player, Minecraft.getInstance().level, @@ -53,7 +50,7 @@ final class HandDynamics { * * @return true, если был pushPose и нужен popPose */ - static boolean apply( + public static boolean apply( HoldMyItems module, HandAnimState state, AbstractClientPlayer player, float tickDelta, InteractionHand hand, PoseStack matrices ) { diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/AntiBotTracker.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/AntiBotTracker.java index bd6cd6d..6aca987 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/AntiBotTracker.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/AntiBotTracker.java @@ -39,7 +39,6 @@ public final class AntiBotTracker { EquipmentSlot.FEET }; - private final Minecraft mc = Minecraft.getInstance(); private final ConcurrentHashMap<UUID, TabInfo> tab = new ConcurrentHashMap<>(); private final ConcurrentHashMap<UUID, SuspectInfo> suspects = new ConcurrentHashMap<>(); private final ConcurrentHashMap.KeySetView<UUID, Boolean> bots = ConcurrentHashMap.newKeySet(); @@ -58,6 +57,7 @@ public final class AntiBotTracker { @EventHandler private void onGameTick(GameTickEvent event) { + Minecraft mc = Minecraft.getInstance(); if (mc.level == null || mc.player == null) { reset(); return; @@ -83,6 +83,7 @@ public final class AntiBotTracker { } public boolean isBot(Player player) { + Minecraft mc = Minecraft.getInstance(); if (player == null || mc.player == null || player == mc.player) { return false; } @@ -150,6 +151,7 @@ public final class AntiBotTracker { } private BotAssessment assess(Player player) { + Minecraft mc = Minecraft.getInstance(); String name = player.getGameProfile().name(); UUID uuid = player.getUUID(); TabInfo info = tab.get(uuid); @@ -206,6 +208,7 @@ public final class AntiBotTracker { } private boolean hasDuplicateProfile(String name, UUID self) { + Minecraft mc = Minecraft.getInstance(); if (name == null || name.isBlank() || self == null) { return false; } @@ -235,6 +238,7 @@ public final class AntiBotTracker { } private boolean isKnownInTab(UUID uuid) { + Minecraft mc = Minecraft.getInstance(); if (uuid == null) { return false; } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/ReachMeter.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/ReachMeter.java new file mode 100644 index 0000000..667e44e --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/combat/ReachMeter.java @@ -0,0 +1,82 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.util.combat; + +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; + +import java.util.Locale; + +/** + * Tracks the reach (eye -> target hitbox distance) of the player's last attack + * for the ReachDisplay HUD widget. Measurement math is pure and unit-tested; + * state is a volatile snapshot written on the attack event. + */ +public final class ReachMeter { + + /** Vanilla survival attack range, m. Distances above are tinted as overreach. */ + public static final double VANILLA_REACH = 3.0; + + private static volatile double lastDistance = -1.0; + private static volatile double sessionMax = -1.0; + private static volatile long lastAttackAtMs; + + private ReachMeter() { + } + + public static void record(double distance, long nowMs) { + if (Double.isNaN(distance) || distance < 0.0) return; + lastDistance = distance; + lastAttackAtMs = nowMs; + if (distance > sessionMax) { + sessionMax = distance; + } + } + + public static double lastDistance() { + return lastDistance; + } + + public static double sessionMax() { + return sessionMax; + } + + public static long lastAttackAtMs() { + return lastAttackAtMs; + } + + public static void reset() { + lastDistance = -1.0; + sessionMax = -1.0; + lastAttackAtMs = 0L; + } + + /** + * Distance from a point to the closest point of an AABB (per-axis clamp). + * Returns 0 when the point is inside the box. + */ + public static double distanceToBox(Vec3 point, AABB box) { + if (point == null || box == null) return -1.0; + double dx = axisDelta(point.x, box.minX, box.maxX); + double dy = axisDelta(point.y, box.minY, box.maxY); + double dz = axisDelta(point.z, box.minZ, box.maxZ); + return Math.sqrt(dx * dx + dy * dy + dz * dz); + } + + private static double axisDelta(double value, double min, double max) { + if (value < min) return min - value; + if (value > max) return value - max; + return 0.0; + } + + /** Formats meters for display, e.g. "3.12 m"; "-" when no measurement yet. */ + public static String format(double distance) { + if (distance < 0.0) return "-"; + return String.format(Locale.ROOT, "%.2f m", distance); + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/media/MediaSessionService.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/media/MediaSessionService.java index 2d9a674..237a07c 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/media/MediaSessionService.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/media/MediaSessionService.java @@ -24,7 +24,6 @@ public final class MediaSessionService { private static final Identifier ARTWORK_ID = Identifier.fromNamespaceAndPath("lovisual", "media_player/artwork"); private static final MediaSessionService INSTANCE = new MediaSessionService(); - private final Minecraft mc = Minecraft.getInstance(); private final MediaPlayerInfo mediaPlayerInfo = MediaPlayerInfo.system(); private volatile List<IMediaSession> mediaSessions = List.of(); private volatile boolean pollingRunning; @@ -338,6 +337,7 @@ public final class MediaSessionService { } private void updateArtwork(MediaInfo media) { + Minecraft mc = Minecraft.getInstance(); artworkTexture = null; if (mc == null || media == null || media.artworkPng() == null || media.artworkPng().length == 0) return; NativeImage image = getArtworkAsNativeImage(media); diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/ElytraRecastUtil.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/ElytraRecastUtil.java index a770b45..2b8a810 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/ElytraRecastUtil.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/ElytraRecastUtil.java @@ -30,12 +30,15 @@ public final class ElytraRecastUtil { private static final int GRIM_COOLDOWN_TICKS = 3; private static final double VANILLA_JUMP_HEADROOM = 0.42; - private final Minecraft client = Minecraft.getInstance(); private final Map<Object, RecastState> states = new IdentityHashMap<>(); private ElytraRecastUtil() { } + private static Minecraft client() { + return Minecraft.getInstance(); + } + private static boolean isElytraUsable(ItemStack stack) { if (stack == null || stack.isEmpty() || !stack.is(Items.ELYTRA)) return false; if (!stack.isDamageableItem()) return true; @@ -69,7 +72,7 @@ public final class ElytraRecastUtil { } if (state.autoWalk) { - client.options.keyUp.setDown(true); + client().options.keyUp.setDown(true); state.forcedForward = true; } @@ -138,7 +141,7 @@ public final class ElytraRecastUtil { } if (state.phase == Phase.VANILLA_START_JUMP) { - LocalPlayer player = client.player; + LocalPlayer player = client().player; if (!canAttempt(player, state)) { reset(owner); return false; @@ -189,10 +192,10 @@ public final class ElytraRecastUtil { } private boolean canAttempt(LocalPlayer player, RecastState state) { - if (player == null || client.options == null) return false; + if (player == null || client().options == null) return false; if (player.isFallFlying() || player.onGround()) return false; if (player.getDeltaMovement().y > -0.01 && player.fallDistance <= 0.0f) return false; - if (state.requireJumpHeld && !client.options.keyJump.isDown()) return false; + if (state.requireJumpHeld && !client().options.keyJump.isDown()) return false; if (player.getAbilities().flying) return false; if (player.isPassenger()) return false; if (player.onClimbable()) return false; @@ -222,14 +225,14 @@ public final class ElytraRecastUtil { if (player != null && player.input != null && player.input.keyPresses != null && player.input.keyPresses.jump()) { return true; } - return client.options != null && client.options.keyJump.isDown(); + return client().options != null && client().options.keyJump.isDown(); } private void releaseForcedKeys(RecastState state) { - if (!state.forcedForward || client.options == null) return; + if (!state.forcedForward || client().options == null) return; state.forcedForward = false; - client.options.keyUp.setDown(false); + client().options.keyUp.setDown(false); KeyMapping.setAll(); } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/inventory/FireworkUseController.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/inventory/FireworkUseController.java index b79a491..95f15fa 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/inventory/FireworkUseController.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/inventory/FireworkUseController.java @@ -28,7 +28,6 @@ public final class FireworkUseController { public static final int DEFAULT_MANUAL_COOLDOWN_TICKS = 20; private static final long MILLIS_PER_TICK = 50L; - private final Minecraft mc = Minecraft.getInstance(); private long lastUseMs; private FireworkUseController() { @@ -41,6 +40,7 @@ public final class FireworkUseController { boolean silent, int cooldownTicks, boolean syncCombat) { + Minecraft mc = Minecraft.getInstance(); LocalPlayer player = mc.player; MultiPlayerGameMode manager = mc.gameMode; if (player == null || manager == null || mc.level == null) return false; @@ -115,6 +115,7 @@ public final class FireworkUseController { } private boolean hasOwnedActiveFirework(LocalPlayer player) { + Minecraft mc = Minecraft.getInstance(); for (Entity entity : mc.level.entitiesForRendering()) { if (entity instanceof FireworkRocketEntity firework && firework.isAlive() diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/inventory/InventorySwap.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/inventory/InventorySwap.java index 593897f..fedcc1c 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/inventory/InventorySwap.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/player/inventory/InventorySwap.java @@ -42,7 +42,6 @@ public final class InventorySwap { private static final int OFFHAND_SCREEN_SLOT = 45; private static final int OFFHAND_SWAP_BUTTON = 40; - private final Minecraft mc = Minecraft.getInstance(); private final Deque<QueuedAction> strictQueue = new ArrayDeque<>(); private int lastServerSelectedSlot = -1; private int savedSelectedSlot = -1; @@ -68,7 +67,8 @@ public final class InventorySwap { } public static int mapInventoryToScreenSlot(int inventorySlot) { - LocalPlayer player = INSTANCE.mc.player; + Minecraft mc = Minecraft.getInstance(); + LocalPlayer player = mc.player; AbstractContainerMenu handler = player != null ? player.containerMenu : null; Container inventory = player != null ? player.getInventory() : null; return mapInventoryToScreenSlot(handler, inventory, inventorySlot); @@ -93,8 +93,9 @@ public final class InventorySwap { } public static int playerInventoryStart(AbstractContainerMenu handler) { + Minecraft mc = Minecraft.getInstance(); if (handler == null) return -1; - LocalPlayer player = INSTANCE.mc.player; + LocalPlayer player = mc.player; Container inventory = player != null ? player.getInventory() : null; if (handler != null && inventory != null) { int min = Integer.MAX_VALUE; @@ -111,8 +112,9 @@ public final class InventorySwap { } public static int hotbarStart(AbstractContainerMenu handler) { + Minecraft mc = Minecraft.getInstance(); if (handler == null) return -1; - LocalPlayer player = INSTANCE.mc.player; + LocalPlayer player = mc.player; Container inventory = player != null ? player.getInventory() : null; if (handler != null && inventory != null) { int min = Integer.MAX_VALUE; @@ -143,6 +145,7 @@ public final class InventorySwap { @EventHandler private void onPacketSend(PacketEvent.Send event) { + Minecraft mc = Minecraft.getInstance(); Object packet = event.getPacket(); if (packet instanceof ServerboundSetCarriedItemPacket selectedSlot) { lastServerSelectedSlot = selectedSlot.getSlot(); @@ -341,6 +344,7 @@ public final class InventorySwap { } public int clientSelectedSlot() { + Minecraft mc = Minecraft.getInstance(); LocalPlayer player = mc.player; if (player == null) return 0; return ((PlayerInventoryAccessor) player.getInventory()).lovisual$getSelectedSlot(); @@ -356,6 +360,7 @@ public final class InventorySwap { } public boolean leaseHotbar(Object owner, int slot, int ticksUntilReset) { + Minecraft mc = Minecraft.getInstance(); LocalPlayer player = mc.player; if (player == null || !isHotbarSlot(slot)) return false; @@ -391,6 +396,7 @@ public final class InventorySwap { } public boolean selectHotbar(int slot) { + Minecraft mc = Minecraft.getInstance(); LocalPlayer player = mc.player; if (player == null || mc.getConnection() == null || !isHotbarSlot(slot)) return false; @@ -408,6 +414,7 @@ public final class InventorySwap { } public boolean sendSelectedSlotIfNeeded(int slot) { + Minecraft mc = Minecraft.getInstance(); if (mc.player == null || mc.getConnection() == null || !isHotbarSlot(slot)) return false; if (lastServerSelectedSlot != slot) { mc.getConnection().send(new ServerboundSetCarriedItemPacket(slot)); @@ -422,6 +429,7 @@ public final class InventorySwap { } public boolean clickSwap(int containerSlot, int hotbarButton) { + Minecraft mc = Minecraft.getInstance(); LocalPlayer player = mc.player; MultiPlayerGameMode interaction = mc.gameMode; AbstractContainerMenu handler = player != null ? player.containerMenu : null; @@ -438,6 +446,7 @@ public final class InventorySwap { } public boolean pickupSwap(int slotA, int slotB) { + Minecraft mc = Minecraft.getInstance(); LocalPlayer player = mc.player; MultiPlayerGameMode interaction = mc.gameMode; AbstractContainerMenu handler = player != null ? player.containerMenu : null; @@ -487,6 +496,7 @@ public final class InventorySwap { } public boolean dropStack(Slot slot) { + Minecraft mc = Minecraft.getInstance(); LocalPlayer player = mc.player; MultiPlayerGameMode interaction = mc.gameMode; AbstractContainerMenu handler = player != null ? player.containerMenu : null; @@ -505,6 +515,7 @@ public final class InventorySwap { } public int quickMoveMatchingFromContainer(Set<String> itemIds) { + Minecraft mc = Minecraft.getInstance(); LocalPlayer player = mc.player; MultiPlayerGameMode interaction = mc.gameMode; AbstractContainerMenu handler = player != null ? player.containerMenu : null; @@ -681,6 +692,7 @@ public final class InventorySwap { } public boolean closeHandledScreen(InventorySwapPolicy policy) { + Minecraft mc = Minecraft.getInstance(); LocalPlayer player = mc.player; if (player == null || mc.getConnection() == null || player.containerMenu == null) return false; @@ -706,6 +718,7 @@ public final class InventorySwap { } public InteractionResult useItem(InteractionHand hand) { + Minecraft mc = Minecraft.getInstance(); LocalPlayer player = mc.player; MultiPlayerGameMode interaction = mc.gameMode; if (player == null || interaction == null || hand == null) return InteractionResult.FAIL; @@ -713,6 +726,7 @@ public final class InventorySwap { } public boolean useItemWith(Predicate<ItemStack> predicate, InventorySwapMode mode, InteractionHand hand, boolean swing) { + Minecraft mc = Minecraft.getInstance(); return withFoundSlot(predicate, mode, () -> { InteractionResult result = useItem(hand); if (swing && result.consumesAction() && mc.player != null) { @@ -723,6 +737,7 @@ public final class InventorySwap { public boolean useItemWith(Predicate<ItemStack> predicate, InventorySwapMode mode, InventorySwapPolicy policy, InteractionHand hand, boolean swing) { + Minecraft mc = Minecraft.getInstance(); return withFoundSlot(predicate, mode, policy, InventoryActionKind.USE_ITEM, () -> { InteractionResult result = useItem(hand); if (swing && result.consumesAction() && mc.player != null) { @@ -732,6 +747,7 @@ public final class InventorySwap { } public boolean swingHand(InteractionHand hand, InventorySwapPolicy policy) { + Minecraft mc = Minecraft.getInstance(); if (mc.player == null || hand == null) return false; Runnable swing = () -> mc.player.swing(hand); if (policy == null || policy == InventorySwapPolicy.NONE || isSafeFor(InventoryActionKind.SWING, policy)) { @@ -751,6 +767,7 @@ public final class InventorySwap { } public SlotResult findHotbar(Predicate<ItemStack> predicate) { + Minecraft mc = Minecraft.getInstance(); LocalPlayer player = mc.player; if (player == null || predicate == null) return SlotResult.empty(); @@ -765,6 +782,7 @@ public final class InventorySwap { } public SlotResult findInventory(Predicate<ItemStack> predicate) { + Minecraft mc = Minecraft.getInstance(); LocalPlayer player = mc.player; if (player == null || predicate == null) return SlotResult.empty(); @@ -853,6 +871,7 @@ public final class InventorySwap { } private boolean isSafeFor(InventoryActionKind kind, InventorySwapPolicy policy) { + Minecraft mc = Minecraft.getInstance(); if (policy == null || policy == InventorySwapPolicy.NONE) return true; LocalPlayer player = mc.player; if (player == null) return false; @@ -931,6 +950,7 @@ public final class InventorySwap { } private void requestMovementLock(int ticks) { + Minecraft mc = Minecraft.getInstance(); movementLockTicks = Math.max(movementLockTicks, Math.max(0, ticks)); if (mc.player != null && mc.player.isSprinting()) { mc.player.setSprinting(false); @@ -942,6 +962,7 @@ public final class InventorySwap { } private HotbarLease activeLease() { + Minecraft mc = Minecraft.getInstance(); HotbarLease current = hotbarLease; if (current == null) return null; @@ -978,6 +999,7 @@ public final class InventorySwap { } private void syncInteractionManagerSlot(int slot) { + Minecraft mc = Minecraft.getInstance(); if (mc.gameMode instanceof MultiPlayerGameModeAccessor accessor) { accessor.lovisual$setLastSelectedSlot(slot); } diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/text/LegacyTextUtil.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/text/LegacyTextUtil.java index 884c688..ed86579 100644 --- a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/text/LegacyTextUtil.java +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/text/LegacyTextUtil.java @@ -178,8 +178,14 @@ public enum LegacyTextUtil { return out; } + private static final java.util.EnumSet<ChatFormatting> COLOR_FORMATS = java.util.EnumSet.of( + ChatFormatting.BLACK, ChatFormatting.DARK_BLUE, ChatFormatting.DARK_GREEN, ChatFormatting.DARK_AQUA, + ChatFormatting.DARK_RED, ChatFormatting.DARK_PURPLE, ChatFormatting.GOLD, ChatFormatting.GRAY, + ChatFormatting.DARK_GRAY, ChatFormatting.BLUE, ChatFormatting.GREEN, ChatFormatting.AQUA, + ChatFormatting.RED, ChatFormatting.LIGHT_PURPLE, ChatFormatting.YELLOW, ChatFormatting.WHITE); + private static boolean isColor(ChatFormatting fmt) { - return fmt.ordinal() <= ChatFormatting.WHITE.ordinal(); + return COLOR_FORMATS.contains(fmt); } public static String ampersandToSection(String s) { diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/text/NameMasker.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/text/NameMasker.java new file mode 100644 index 0000000..bf19a5d --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/text/NameMasker.java @@ -0,0 +1,121 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.util.text; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Pure name/word masking shared by StreamerMode. Unlike {@link NameProtector} + * it masks every nickname-like token deterministically (no per-name config): + * "Notch" -> "Player", "Herobrine_777" -> "Player2". + */ +public final class NameMasker { + + private static final int TOKEN_MAX = 16; + private static final java.util.Set<String> COMMON_WORDS = java.util.Set.of( + "and", "или", "the", "for", "you", "your", "has", "was", "not", "все", "для" + ); + + private final Map<String, String> assigned = new LinkedHashMap<>(); + private final String replacement; + private final List<String> extraWords; + + public NameMasker(String replacement, List<String> extraWords) { + this.replacement = replacement == null || replacement.isBlank() ? "Player" : replacement.trim(); + this.extraWords = extraWords == null ? List.of() : List.copyOf(extraWords); + } + + public String mask(String text) { + if (text == null || text.isEmpty()) return text; + + String masked = replaceWords(text); + if (!masked.equals(text)) return masked; + + return replaceTokens(masked); + } + + private String replaceTokens(String text) { + StringBuilder out = new StringBuilder(text.length()); + int i = 0; + while (i < text.length()) { + int tokenEnd = nicknameTokenEnd(text, i); + if (tokenEnd < 0) { + out.append(text.charAt(i)); + i++; + continue; + } + String token = text.substring(i, tokenEnd); + String alias = assigned.computeIfAbsent(token, this::registerTokenAlias); + out.append(alias); + i = tokenEnd; + } + return out.toString(); + } + + private String registerTokenAlias(String token) { + return replacement + (assigned.size() + 1); + } + + private String replaceWords(String text) { + String result = text; + for (String word : extraWords) { + if (word == null || word.isBlank()) continue; + result = replaceAllIgnoreCase(result, word.trim(), replacement); + } + return result; + } + + static String replaceAllIgnoreCase(String text, String word, String replacement) { + String lower = text.toLowerCase(java.util.Locale.ROOT); + String needle = word.toLowerCase(java.util.Locale.ROOT); + StringBuilder out = new StringBuilder(text.length()); + int i = 0; + while (i < text.length()) { + int idx = lower.indexOf(needle, i); + if (idx < 0) { + out.append(text, i, text.length()); + break; + } + out.append(text, i, idx).append(replacement); + i = idx + needle.length(); + } + return out.toString(); + } + + /** + * Nickname-like token: latin letters/digits/underscore, 3..16 chars, + * containing at least one letter and at least one digit-free letter run + * (skips pure numbers and common words like "and"). + */ + static int nicknameTokenEnd(String text, int start) { + int end = start; + while (end < text.length() && end - start < TOKEN_MAX && isTokenChar(text.charAt(end))) end++; + if (end - start < 3) return -1; + + String token = text.substring(start, end).toLowerCase(java.util.Locale.ROOT); + if (COMMON_WORDS.contains(token)) return -1; + + boolean hasLetter = false; + for (int i = start; i < end; i++) { + if (Character.isLetter(text.charAt(i))) { + hasLetter = true; + break; + } + } + boolean wordBounded = + (start == 0 || !isTokenChar(text.charAt(start - 1))) + && (end >= text.length() || !isTokenChar(text.charAt(end))); + return hasLetter && wordBounded ? end : -1; + } + + private static boolean isTokenChar(char c) { + return Character.isLetterOrDigit(c) || c == '_'; + } +} diff --git a/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/text/NameProtector.java b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/text/NameProtector.java new file mode 100644 index 0000000..da50be1 --- /dev/null +++ b/combatant-client-26.2/src/main/java/dev/loki/lovisual/util/text/NameProtector.java @@ -0,0 +1,85 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.util.text; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Pure name-replacement logic for NameProtect: replaces occurrences of real + * nicknames with display aliases inside plain strings (case-insensitive, + * bounded matches only). Component-tree rewriting happens in the mixins. + */ +public final class NameProtector { + + private final Map<String, String> aliases = new LinkedHashMap<>(); + + /** + * @param realName nickname to hide; null/blank is ignored + * @param alias replacement text; blank keeps the alias empty + */ + public void put(String realName, String alias) { + if (realName == null || realName.isBlank()) return; + aliases.put(realName, alias == null ? "" : alias); + } + + public void clear() { + aliases.clear(); + } + + public boolean isEmpty() { + return aliases.isEmpty(); + } + + public String replace(String text) { + if (text == null || text.isEmpty() || aliases.isEmpty()) return text; + + List<String> names = new ArrayList<>(aliases.keySet()); + names.sort((a, b) -> Integer.compare(b.length(), a.length())); + + StringBuilder out = new StringBuilder(text.length()); + int i = 0; + outer: + while (i < text.length()) { + for (String name : names) { + if (!matchesAt(text, i, name)) continue; + String alias = aliases.get(name); + out.append(alias); + i += name.length(); + continue outer; + } + out.append(text.charAt(i)); + i++; + } + return out.toString(); + } + + /** + * Replace only when the whole string equals the name — used for scoreboards + * and tab entries where the display name IS the nickname. + */ + public String replaceExact(String text) { + if (text == null || text.isBlank() || aliases.isEmpty()) return text; + String alias = aliases.get(text.trim()); + return alias != null ? alias : text; + } + + private static boolean matchesAt(String text, int start, String name) { + if (!text.regionMatches(true, start, name, 0, name.length())) return false; + if (start > 0 && isWordChar(text.charAt(start - 1))) return false; + int end = start + name.length(); + if (end < text.length() && isWordChar(text.charAt(end))) return false; + return true; + } + + private static boolean isWordChar(char c) { + return c == '_' || Character.isLetterOrDigit(c); + } +} 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 8586cb7..ad609e1 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 @@ -544,6 +544,45 @@ "setting.freecam.vertical_speed": "Vertical speed", "setting.freecam.toggle_input": "Toggle input", "setting.fullbright.min_light": "Min light", + "module.light.description": "Tints the world ambient light with a custom color.", + "setting.light.tint_ambient": "Tint ambient", + "setting.light.tint_sky": "Tint skylight", + "setting.light.color": "Color", + "setting.light.strength": "Strength", + "module.hitboxes.description": "Colored hitbox outlines. Render-only, collisions untouched.", + "setting.hitboxes.render_mode": "Render mode", + "setting.hitboxes.render_mode.option.Outline": "Outline", + "setting.hitboxes.render_mode.option.Fill": "Fill", + "setting.hitboxes.render_mode.option.Both": "Both", + "setting.hitboxes.color_mode": "Color mode", + "setting.hitboxes.color_mode.option.Sync": "Sync", + "setting.hitboxes.color_mode.option.Custom": "Custom", + "setting.hitboxes.eyes_line": "Eyes line", + "setting.hitboxes.players_only": "Players only", + "setting.hitboxes.line_width": "Line width", + "setting.hitboxes.fill_alpha": "Fill alpha", + "setting.hitboxes.color": "Color", + "setting.hitboxes.friend_color": "Friend color", + "module.nameprotect.description": "Replaces your and friends' nicknames with an alias in chat and tab.", + "setting.nameprotect.alias": "Alias", + "setting.nameprotect.protect_self": "Protect self", + "setting.nameprotect.protect_friends": "Protect friends", + "setting.hud_element.watermark.scale": "Scale", + "setting.hud_element.reach_display.scale": "Scale", + "setting.hud_element.reach_display.keep_ms": "Keep on screen (ms)", + "setting.hud_element.reach_display.sync_theme": "Sync with theme", + "setting.hud_element.reach_display.show_max": "Show session max", + "setting.hud_element.reach_display.highlight_overreach": "Highlight overreach", + "setting.hud_element.reach_display.value_color": "Text color", + "setting.hud_element.reach_display.bg_color": "Background", + "setting.hud_element.watermark.sync_theme": "Sync with theme", + "setting.hud_element.watermark.show_version": "Show version", + "setting.hud_element.watermark.show_user": "Show username", + "setting.hud_element.watermark.show_fps": "Show FPS", + "setting.hud_element.watermark.show_ping": "Show ping", + "setting.hud_element.watermark.title_color": "Brand color", + "setting.hud_element.watermark.meta_color": "Meta color", + "setting.hud_element.watermark.bg_color": "Background", "setting.soundesp.name_mode": "Name mode", "setting.soundesp.name_mode.option.Technical": "Technical", "setting.soundesp.name_mode.option.Localized": "Localized", @@ -3199,7 +3238,7 @@ "command.coordinates.description": "Shows the current player coordinates.", "command.enemy.description": "Manages the client enemy list.", "command.friend.description": "Manages the client friend list.", - "command.help.description": "Shows every available client command with clickable pages.", + "command.help.description": "Shows every available client command with clickable pages. Prefix: % (legacy @ also works).", "command.hide.description": "Hides or shows a module in ModuleList without disabling it.", "command.iris.description": "Shows Iris and shader compatibility diagnostics.", "command.modules.description": "Lists modules by state or category.", @@ -3219,9 +3258,7 @@ "setting.chinahat.rim_color": "Rim color", "setting.chinahat.segments": "Segments", "setting.chinahat.brim_radius": "Brim radius", - "setting.chinahat.dome_radius": "Dome radius", - "setting.chinahat.dome_height": "Dome height", - "setting.chinahat.brim_thickness": "Brim thickness", + "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", @@ -3263,5 +3300,42 @@ "setting.holdmyitems.bob_strength": "Bobbing strength", "setting.holdmyitems.use_drift": "Using-item drift", "setting.holdmyitems.drift_strength": "Drift strength", - "setting.holdmyitems.join_appear": "Smooth world entry" + "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", + "setting.autoleave.ignore_friends": "Ignore friends", + "setting.autoleave.delay": "Delay", + "module.autoaccept.description": "Automatically accepts teleport and clan requests by sending a command.", + "setting.autoaccept.patterns": "Request patterns", + "setting.autoaccept.command": "Answer command", + "setting.autoaccept.only_friends": "Only from friends", + "setting.autoaccept.delay": "Delay", + "setting.autoaccept.cooldown": "Cooldown", + "module.usetracker.description": "Announces totem pops and selected effects used by other players.", + "setting.usetracker.self_totems": "My totem pops", + "setting.usetracker.other_totems": "Others' totem pops", + "setting.usetracker.other_effects": "Tracked effects", + "setting.usetracker.other_effects.option.invisibility": "Invisibility", + "setting.usetracker.other_effects.option.speed": "Speed", + "setting.usetracker.other_effects.option.strength": "Strength", + "setting.usetracker.other_effects.option.regeneration": "Regeneration", + "setting.usetracker.other_effects.option.fire_resistance": "Fire Resistance", + "setting.usetracker.other_effects.option.weakness": "Weakness", + "setting.nosound.mode": "Mode", + "setting.nosound.mode.option.Mute": "Mute", + "setting.nosound.mode.option.Reduce": "Reduce", + "setting.nosound.volume_scale": "Volume scale", + "module.thirdperson.description": "Free third-person camera look while the bind is held, without turning your movement.", + "setting.thirdperson.distance": "Distance", + "module.streamermode.description": "Streamer mode: masks nicknames and server-brand words in chat, tab and scoreboard, and can replace skins with the default one.", + "setting.streamermode.alias": "Replacement alias", + "setting.streamermode.mask_chat": "Mask nicknames", + "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" } 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 3f95ed6..c3f104b 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 @@ -479,6 +479,45 @@ "setting.freecam.vertical_speed": "Вертикальная скорость", "setting.freecam.toggle_input": "Переключить ввод", "setting.fullbright.min_light": "Мин. свет", + "module.light.description": "Окрашивает окружающее освещение в выбранный цвет.", + "setting.light.tint_ambient": "Тонировать ambient", + "setting.light.tint_sky": "Тонировать skylight", + "setting.light.color": "Цвет", + "setting.light.strength": "Сила", + "module.hitboxes.description": "Цветные контуры хитбоксов. Только рендер, коллизии не меняются.", + "setting.hitboxes.render_mode": "Режим отрисовки", + "setting.hitboxes.render_mode.option.Outline": "Контур", + "setting.hitboxes.render_mode.option.Fill": "Заливка", + "setting.hitboxes.render_mode.option.Both": "Оба", + "setting.hitboxes.color_mode": "Режим цвета", + "setting.hitboxes.color_mode.option.Sync": "Синхрон", + "setting.hitboxes.color_mode.option.Custom": "Свой", + "setting.hitboxes.eyes_line": "Линия взгляда", + "setting.hitboxes.players_only": "Только игроки", + "setting.hitboxes.line_width": "Толщина линии", + "setting.hitboxes.fill_alpha": "Альфа заливки", + "setting.hitboxes.color": "Цвет", + "setting.hitboxes.friend_color": "Цвет друзей", + "module.nameprotect.description": "Заменяет ваш ник и ники друзей на псевдоним в чате и таб-листе.", + "setting.nameprotect.alias": "Псевдоним", + "setting.nameprotect.protect_self": "Защищать себя", + "setting.nameprotect.protect_friends": "Защищать друзей", + "setting.hud_element.watermark.scale": "Масштаб", + "setting.hud_element.reach_display.scale": "Масштаб", + "setting.hud_element.reach_display.keep_ms": "Держать на экране (мс)", + "setting.hud_element.reach_display.sync_theme": "Синхрон с темой", + "setting.hud_element.reach_display.show_max": "Показывать максимум", + "setting.hud_element.reach_display.highlight_overreach": "Подсвечивать перебор", + "setting.hud_element.reach_display.value_color": "Цвет текста", + "setting.hud_element.reach_display.bg_color": "Фон", + "setting.hud_element.watermark.sync_theme": "Синхрон с темой", + "setting.hud_element.watermark.show_version": "Показывать версию", + "setting.hud_element.watermark.show_user": "Показывать ник", + "setting.hud_element.watermark.show_fps": "Показывать FPS", + "setting.hud_element.watermark.show_ping": "Показывать пинг", + "setting.hud_element.watermark.title_color": "Цвет бренда", + "setting.hud_element.watermark.meta_color": "Цвет мета-данных", + "setting.hud_element.watermark.bg_color": "Фон", "setting.soundesp.name_mode": "Режим имени", "setting.soundesp.name_mode.option.Technical": "Технический", "setting.soundesp.name_mode.option.Localized": "Локализованный", @@ -2989,7 +3028,7 @@ "command.coordinates.description": "Показывает текущие координаты игрока.", "command.enemy.description": "Управляет списком врагов клиента.", "command.friend.description": "Управляет списком друзей клиента.", - "command.help.description": "Показывает все доступные команды клиента с кликабельными страницами.", + "command.help.description": "Показывает все доступные команды клиента с кликабельными страницами. Префикс: % (старый @ тоже работает).", "command.hide.description": "Скрывает или показывает модуль в ModuleList, не отключая его.", "command.iris.description": "Показывает диагностику совместимости Iris и шейдеров.", "command.modules.description": "Показывает модули по состоянию или категории.", @@ -3009,9 +3048,7 @@ "setting.chinahat.rim_color": "Цвет обода", "setting.chinahat.segments": "Сегменты", "setting.chinahat.brim_radius": "Радиус полей", - "setting.chinahat.dome_radius": "Радиус купола", - "setting.chinahat.dome_height": "Высота купола", - "setting.chinahat.brim_thickness": "Толщина полей", + "setting.chinahat.cone_height": "Высота конуса", "setting.chinahat.head_top_offset": "Высота на голове", "setting.chinahat.head_forward_offset": "Смещение вперёд", "setting.chinahat.head_right_offset": "Смещение вправо", @@ -3053,5 +3090,42 @@ "setting.holdmyitems.bob_strength": "Сила покачивания", "setting.holdmyitems.use_drift": "Дрейф при использовании", "setting.holdmyitems.drift_strength": "Сила дрейфа", - "setting.holdmyitems.join_appear": "Плавное появление при входе" + "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": "Радиус", + "setting.autoleave.ignore_friends": "Игнорировать друзей", + "setting.autoleave.delay": "Задержка", + "module.autoaccept.description": "Автоматически принимает запросы телепортации и клановые, отправляя команду.", + "setting.autoaccept.patterns": "Шаблоны запросов", + "setting.autoaccept.command": "Команда ответа", + "setting.autoaccept.only_friends": "Только от друзей", + "setting.autoaccept.delay": "Задержка", + "setting.autoaccept.cooldown": "Перезарядка", + "module.usetracker.description": "Сообщает о тотемах и выбранных эффектах, применённых другими игроками.", + "setting.usetracker.self_totems": "Мои тотемы", + "setting.usetracker.other_totems": "Тотемы других", + "setting.usetracker.other_effects": "Отслеживаемые эффекты", + "setting.usetracker.other_effects.option.invisibility": "Невидимость", + "setting.usetracker.other_effects.option.speed": "Скорость", + "setting.usetracker.other_effects.option.strength": "Сила", + "setting.usetracker.other_effects.option.regeneration": "Регенерация", + "setting.usetracker.other_effects.option.fire_resistance": "Огнестойкость", + "setting.usetracker.other_effects.option.weakness": "Слабость", + "setting.nosound.mode": "Режим", + "setting.nosound.mode.option.Mute": "Выключать", + "setting.nosound.mode.option.Reduce": "Приглушать", + "setting.nosound.volume_scale": "Громкость", + "module.thirdperson.description": "Свободный обзор камеры от третьего лица при удержании бинда, без поворота движения.", + "setting.thirdperson.distance": "Дистанция", + "module.streamermode.description": "Режим стримера: маскирует ники и названия серверов в чате, табе и скорборде, может заменять скины на стандартный.", + "setting.streamermode.alias": "Текст замены", + "setting.streamermode.mask_chat": "Маскировать ники", + "setting.streamermode.mask_server_words": "Маскировать названия серверов", + "setting.streamermode.hide_skins": "Скрывать скины", + "module.itemscroller.description": "Перекладывает предметы колёсиком мыши (shift-клик) в любом инвентаре.", + "setting.itemscroller.hotbar_fallback": "Скролл двигает слот хотбара" } diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_1.png b/combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_1.png index 434e26c..09cd400 100644 Binary files a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_1.png and b/combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_1.png differ diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_2.png b/combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_2.png index 3ce9259..e0c587c 100644 Binary files a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_2.png and b/combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_2.png differ diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_3.png b/combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_3.png index 1d82eb5..f47dd77 100644 Binary files a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_3.png and b/combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_3.png differ diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_4.png b/combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_4.png index 6524597..db46a65 100644 Binary files a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_4.png and b/combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_4.png differ diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_5.png b/combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_5.png index 09353f8..f84f8bb 100644 Binary files a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_5.png and b/combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_5.png differ diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_6.png b/combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_6.png index ee63cdc..946a615 100644 Binary files a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_6.png and b/combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_6.png differ diff --git a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_classic.png b/combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_classic.png index 5e5812b..5ab581c 100644 Binary files a/combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_classic.png and b/combatant-client-26.2/src/main/resources/assets/lovisual/textures/trollface/trollface_classic.png differ 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 fc3af4b..aed0e13 100644 --- a/combatant-client-26.2/src/main/resources/lovisual.mixins.json +++ b/combatant-client-26.2/src/main/resources/lovisual.mixins.json @@ -109,6 +109,7 @@ "RenderPipelineMixin", "ScreenEffectRendererMixin", "ScreenMixin", + "SoundEngineMixin", "ServerboundUseItemPacketMixin", "ShaderLoaderMixin", "SkyRendererMixin", diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/config/profile/ConfigProfileDiffEngineTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/config/profile/ConfigProfileDiffEngineTest.java new file mode 100644 index 0000000..2c86345 --- /dev/null +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/config/profile/ConfigProfileDiffEngineTest.java @@ -0,0 +1,122 @@ +/* + * 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.config.profile; + +import org.junit.jupiter.api.Test; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class ConfigProfileDiffEngineTest { + + private final ConfigProfileDiffEngine engine = new ConfigProfileDiffEngine(); + + @Test + void identicalSnapshotsProduceEmptyDiff() { + ConfigProfileSnapshot current = snapshot(entry("mod.a", "Mod A", "x", 1)); + ConfigProfileSnapshot selected = snapshot(entry("mod.a", "Mod A", "x", 1)); + + assertTrue(engine.compare(current, selected).isEmpty()); + } + + @Test + void changedValueIsReported() { + ConfigProfileDiff diff = engine.compare( + snapshot(entry("mod.a", "Mod A", "x", 1)), + snapshot(entry("mod.a", "Mod A", "x", 2))); + + List<ConfigProfileDiff.Value> values = diff.entries().get(0).values(); + assertEquals(1, values.size()); + assertEquals("x", values.get(0).key()); + assertEquals(1, values.get(0).currentValue()); + assertEquals(2, values.get(0).selectedValue()); + assertEquals(ConfigProfileDiff.Kind.CHANGED, values.get(0).kind()); + } + + @Test + void addedAndRemovedValuesAreReported() { + ConfigProfileDiff diff = engine.compare( + snapshot(entry("mod.a", "Mod A", "keep", 1, "gone", 2)), + snapshot(entry("mod.a", "Mod A", "keep", 1, "added", 3))); + + Map<String, ConfigProfileDiff.Kind> kinds = diff.entries().get(0).values().stream() + .collect(Collectors.toMap(ConfigProfileDiff.Value::key, ConfigProfileDiff.Value::kind)); + + assertEquals(Set.of("gone", "added"), kinds.keySet()); + assertEquals(ConfigProfileDiff.Kind.REMOVED, kinds.get("gone")); + assertEquals(ConfigProfileDiff.Kind.ADDED, kinds.get("added")); + } + + @Test + void ownerOnlyInSelectedBecomesAddedEntry() { + ConfigProfileDiff diff = engine.compare( + snapshot(), + snapshot(entry("mod.b", "Mod B", "x", 1))); + + ConfigProfileDiff.Entry entry = diff.entries().get(0); + assertEquals("mod.b", entry.ownerId()); + assertEquals("Mod B", entry.displayName()); + assertEquals(List.of("x"), entry.values().stream() + .map(ConfigProfileDiff.Value::key).toList()); + assertEquals(ConfigProfileDiff.Kind.ADDED, entry.values().get(0).kind()); + assertNull(entry.values().get(0).currentValue()); + } + + @Test + void ownerOnlyInCurrentKeepsCurrentDisplayName() { + ConfigProfileDiff diff = engine.compare( + snapshot(entry("mod.a", "Current Name", "x", 1)), + snapshot()); + + ConfigProfileDiff.Entry entry = diff.entries().get(0); + assertEquals("mod.a", entry.ownerId()); + assertEquals("Current Name", entry.displayName()); + assertEquals(ConfigProfileDiff.Kind.REMOVED, entry.values().get(0).kind()); + assertNull(entry.values().get(0).selectedValue()); + } + + @Test + void nullSnapshotsYieldEmptyModulesDiff() { + ConfigProfileSnapshot snapshot = snapshot(entry("mod.a", "Mod A", "x", 1)); + + assertTrue(engine.compare(null, snapshot).isEmpty()); + assertTrue(engine.compare(snapshot, null).isEmpty()); + assertEquals(ConfigProfileType.MODULES, engine.compare(null, snapshot).type()); + } + + @Test + void valueChangeCountSumsAllChangedValues() { + ConfigProfileDiff diff = engine.compare( + snapshot(entry("mod.a", "A", "x", 1, "y", 2), entry("mod.b", "B", "z", 3)), + snapshot(entry("mod.a", "A", "x", 9), entry("mod.b", "B", "z", 3))); + + // mod.a: x changed, y removed; mod.b identical -> no entry + assertEquals(2, diff.valueChangeCount()); + } + + private static ConfigProfileSnapshot snapshot(ConfigProfileEntry... entries) { + ConfigProfileMeta meta = new ConfigProfileMeta( + "id", "Profile", ConfigProfileType.MODULES, "author", 0L, 0L, 0); + return new ConfigProfileSnapshot(meta, List.of(entries)); + } + + private static ConfigProfileEntry entry(String owner, String displayName, Object... keyValuePairs) { + Map<String, Object> values = new LinkedHashMap<>(); + for (int i = 0; i < keyValuePairs.length; i += 2) { + values.put((String) keyValuePairs[i], keyValuePairs[i + 1]); + } + return new ConfigProfileEntry(owner, displayName, values); + } +} diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/config/profile/ConfigProfileValueCodecTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/config/profile/ConfigProfileValueCodecTest.java new file mode 100644 index 0000000..c12653e --- /dev/null +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/config/profile/ConfigProfileValueCodecTest.java @@ -0,0 +1,122 @@ +/* + * 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.config.profile; + +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +final class ConfigProfileValueCodecTest { + + @Test + void roundTripsNull() throws IOException { + assertNull(roundTrip(null)); + } + + @Test + void roundTripsBooleans() throws IOException { + assertEquals(Boolean.TRUE, roundTrip(Boolean.TRUE)); + assertEquals(Boolean.FALSE, roundTrip(Boolean.FALSE)); + } + + @Test + void roundTripsIntegersIncludingVarIntBoundaries() throws IOException { + assertEquals(0, roundTrip(0)); + assertEquals(127, roundTrip(127)); + assertEquals(128, roundTrip(128)); + assertEquals(Integer.MAX_VALUE, roundTrip(Integer.MAX_VALUE)); + assertEquals(-1, roundTrip(-1)); + } + + @Test + void roundTripsLongsFloatsAndDoubles() throws IOException { + assertEquals(Long.MAX_VALUE, roundTrip(Long.MAX_VALUE)); + assertEquals(1.5f, roundTrip(1.5f)); + assertEquals(2.25d, roundTrip(2.25d)); + } + + @Test + void narrowNumericTypesAreStoredAsDoubles() throws IOException { + assertEquals(7.0d, roundTrip((short) 7)); + assertEquals(8.0d, roundTrip((byte) 8)); + } + + @Test + void roundTripsUtf8Strings() throws IOException { + assertEquals("привет ✨ мир", roundTrip("привет ✨ мир")); + assertEquals("", roundTrip("")); + } + + @Test + void roundTripsListsAndMaps() throws IOException { + List<Object> list = Arrays.asList("a", 1, Boolean.TRUE, null); + assertEquals(list, roundTrip(list)); + + Map<String, Object> map = new LinkedHashMap<>(); + map.put("k", 1); + map.put("empty", null); + map.put("nested", List.of("x")); + assertEquals(map, roundTrip(map)); + } + + @Test + void unknownTypesFallBackToString() throws IOException { + Object value = new Object() { + @Override + public String toString() { + return "opaque"; + } + }; + assertEquals("opaque", roundTrip(value)); + } + + @Test + void unknownTypeByteFailsWithIOException() { + byte[] corrupt = new byte[]{99}; + assertThrows(IOException.class, () -> read(corrupt)); + } + + @Test + void oversizedVarIntFailsWithIOException() { + byte[] corrupt = new byte[]{(byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80, (byte) 0x80}; + assertThrows(IOException.class, () -> read(corrupt)); + } + + @Test + void truncatedStringFailsWithIOException() { + // заголовок длины 5, но самих байтов нет + byte[] corrupt = new byte[]{5}; + assertThrows(IOException.class, () -> read(corrupt)); + } + + private static Object roundTrip(Object value) throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try (DataOutputStream out = new DataOutputStream(bytes)) { + ConfigProfileValueCodec.writeValue(out, value); + } + return read(bytes.toByteArray()); + } + + private static Object read(byte[] data) throws IOException { + try (DataInputStream in = new DataInputStream(new ByteArrayInputStream(data))) { + return ConfigProfileValueCodec.readValue(in); + } + } +} diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/config/values/ColorValueTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/config/values/ColorValueTest.java new file mode 100644 index 0000000..06dfb37 --- /dev/null +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/config/values/ColorValueTest.java @@ -0,0 +1,128 @@ +/* + * 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.config.values; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class ColorValueTest { + + @Test + void rgbNormalizesSupportedFormats() { + assertEquals("#FF0000", new RGBColorValue("c", "#ff0000").get()); + assertEquals("#00FF00", new RGBColorValue("c", "0x00ff00").get()); + assertEquals("#FF0000", new RGBColorValue("c", "#CCFF0000").get()); + assertEquals("#FFFFFF", new RGBColorValue("c", "not-a-color").get()); + assertEquals("#FFFFFF", new RGBColorValue("c", null).get()); + assertEquals("#FFFFFF", new RGBColorValue("c", "").get()); + } + + @Test + void rgbNormalizesRainbowValues() { + assertEquals("RNB#123456", new RGBColorValue("c", "rainbow:#123456").get()); + assertEquals("RNB", new RGBColorValue("c", "RNB").get()); + } + + @Test + void rgbGetArgbAddsFullAlpha() { + assertEquals(0xFFFF0000, new RGBColorValue("c", "#FF0000").getArgb()); + } + + @Test + void rgbRainbowKeepsFullAlpha() { + int argb = new RGBColorValue("c", "RNB#0000FF").getArgb(); + assertEquals(0xFF000000, argb & 0xFF000000); + } + + @Test + void rgbRainbowHelpers() { + RGBColorValue value = new RGBColorValue("c", "RNB#00FF00"); + + assertTrue(value.isRainbow()); + assertFalse(value.supportsAlpha()); + assertEquals("#00FF00", value.rainbowFallbackHex()); + } + + @Test + void rgbToRainbowKeepsBaseColor() { + assertEquals("RNB#FF0000", RGBColorValue.toRainbow("#FF0000")); + } + + @Test + void rgbFromJsonNormalizes() { + RGBColorValue value = new RGBColorValue("c", "#FFFFFF"); + + value.fromJson("0x00ff00"); + assertEquals("#00FF00", value.get()); + + value.fromJson(42); + assertEquals("#00FF00", value.get()); + } + + @Test + void rgbaNormalizesSupportedFormats() { + assertEquals("#FF0000", new RGBAColorValue("c", "#ff0000").get()); + assertEquals("#80FF0000", new RGBAColorValue("c", "#80FF0000").get()); + assertEquals("RGBA#11223344", new RGBAColorValue("c", "rgba:11223344").get()); + // normalize только приводит к '#'+hex; невалидный hex остаётся как есть, getArgb даёт белый + assertEquals("#NOPE", new RGBAColorValue("c", "nope").get()); + assertEquals(0xFFFFFFFF, new RGBAColorValue("c", "nope").getArgb()); + assertEquals("#FFFFFFFF", new RGBAColorValue("c", null).get()); + } + + @Test + void rgbaNormalizesRainbowValues() { + assertEquals("RNB#FFFF0000", new RGBAColorValue("c", "RNB#FF0000").get()); + assertEquals("RNB#80FF0000", new RGBAColorValue("c", "rainbow:80FF0000").get()); + } + + @Test + void rgbaGetArgbParsesAarrggbb() { + assertEquals(0x80FF0000, new RGBAColorValue("c", "#80FF0000").getArgb()); + } + + @Test + void rgbaGetArgbInterpretsRgbaPrefixAsRrggbbAA() { + assertEquals(0x44112233, new RGBAColorValue("c", "RGBA#11223344").getArgb()); + } + + @Test + void rgbaGetArgbAddsFullAlphaToSixDigits() { + assertEquals(0xFFFF0000, new RGBAColorValue("c", "#FF0000").getArgb()); + } + + @Test + void rgbaRainbowKeepsConfiguredAlpha() { + int argb = new RGBAColorValue("c", "RNB#80FF0000").getArgb(); + assertEquals(0x80000000, argb & 0xFF000000); + } + + @Test + void rgbaSupportsAlphaAndRainbowHelpers() { + RGBAColorValue value = new RGBAColorValue("c", "RNB#80FF0000"); + + assertTrue(value.isRainbow()); + assertTrue(value.supportsAlpha()); + assertEquals("#80FF0000", value.rainbowFallbackHex()); + assertEquals("RNB#80FF0000", RGBAColorValue.toRainbow("#80FF0000")); + } + + @Test + void rgbaFromJsonNormalizes() { + RGBAColorValue value = new RGBAColorValue("c", "#FFFFFFFF"); + + value.fromJson("RGBA:11223344"); + assertEquals("RGBA#11223344", value.get()); + + value.fromJson(42); + assertEquals("RGBA#11223344", value.get()); + } +} diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/config/values/EnumValueTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/config/values/EnumValueTest.java new file mode 100644 index 0000000..0524e90 --- /dev/null +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/config/values/EnumValueTest.java @@ -0,0 +1,112 @@ +/* + * 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.config.values; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +final class EnumValueTest { + + private enum Speed implements EnumValue.IdProvider, EnumValue.AliasProvider { + FAST("fast-id", List.of("f", "turbo")), + SLOW("slow-id", List.of()); + + private final String id; + private final List<String> aliases; + + Speed(String id, List<String> aliases) { + this.id = id; + this.aliases = aliases; + } + + @Override + public String getId() { + return id; + } + + @Override + public List<String> aliases() { + return aliases; + } + } + + private enum Plain { + ON, + OFF + } + + @Test + void idProviderUsedForJsonAndLookup() { + EnumValue<Speed> value = new EnumValue<>("e", Speed.FAST, Speed.FAST, Speed.SLOW); + + assertEquals("fast-id", value.getId()); + assertEquals("fast-id", value.toJson()); + assertEquals(List.of("fast-id", "slow-id"), value.getOptions()); + } + + @Test + void setByIdIsCaseInsensitive() { + EnumValue<Speed> value = new EnumValue<>("e", Speed.FAST, Speed.FAST, Speed.SLOW); + + value.setById("SLOW-ID"); + assertEquals(Speed.SLOW, value.get()); + } + + @Test + void aliasesResolveToConstants() { + EnumValue<Speed> value = new EnumValue<>("e", Speed.FAST, Speed.FAST, Speed.SLOW); + + value.setById("turbo"); + assertEquals(Speed.FAST, value.get()); + + value.setById("f"); + assertEquals(Speed.FAST, value.get()); + } + + @Test + void unknownIdKeepsCurrentValue() { + EnumValue<Speed> value = new EnumValue<>("e", Speed.SLOW, Speed.FAST, Speed.SLOW); + + value.setById("nope"); + value.setById(null); + assertEquals(Speed.SLOW, value.get()); + } + + @Test + void fromJsonDelegatesToSetById() { + EnumValue<Speed> value = new EnumValue<>("e", Speed.FAST, Speed.FAST, Speed.SLOW); + + value.fromJson("SLOW-ID"); + assertEquals(Speed.SLOW, value.get()); + + value.fromJson(42); + value.fromJson(null); + assertEquals(Speed.SLOW, value.get()); + } + + @Test + void defaultFallsBackToFirstOptionWhenAbsent() { + EnumValue<Speed> value = new EnumValue<>("e", Speed.FAST, Speed.SLOW); + + assertEquals(Speed.SLOW, value.get()); + } + + @Test + void plainEnumUsesLowercaseNameAsId() { + EnumValue<Plain> value = new EnumValue<>("p", Plain.ON, Plain.class); + + assertEquals("on", value.getId()); + assertEquals("on", value.toJson()); + + value.setById("OFF"); + assertEquals("off", value.toJson()); + } +} diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/config/values/ModeValueTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/config/values/ModeValueTest.java new file mode 100644 index 0000000..7572d95 --- /dev/null +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/config/values/ModeValueTest.java @@ -0,0 +1,91 @@ +/* + * 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.config.values; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class ModeValueTest { + + @Test + void invalidDefaultFallsBackToFirstOption() { + assertEquals("a", new ModeValue("m", "bogus", "a", "b", "c").get()); + } + + @Test + void nextWrapsAroundOptions() { + ModeValue value = new ModeValue("m", "a", "a", "b", "c"); + + value.next(); + assertEquals("b", value.get()); + value.next(); + assertEquals("c", value.get()); + value.next(); + assertEquals("a", value.get()); + } + + @Test + void addOptionTrimsAndRejectsDuplicates() { + ModeValue value = new ModeValue("m", "a", "a", "b"); + + assertTrue(value.addOption(" c ")); + assertEquals(List.of("a", "b", "c"), value.getOptions()); + + assertFalse(value.addOption("c")); + assertFalse(value.addOption(null)); + assertFalse(value.addOption(" ")); + assertEquals(List.of("a", "b", "c"), value.getOptions()); + } + + @Test + void removeOptionCannotRemoveCurrentValue() { + ModeValue value = new ModeValue("m", "a", "a", "b"); + + assertFalse(value.removeOption("a")); + assertEquals(List.of("a", "b"), value.getOptions()); + + assertTrue(value.removeOption(" b ")); + assertEquals(List.of("a"), value.getOptions()); + } + + @Test + void addOptionsCountsAddedEntries() { + ModeValue value = new ModeValue("m", "a", "a"); + + assertEquals(2, value.addOptions(List.of("b", " c ", "b"))); + assertEquals(0, value.addOptions(null)); + assertEquals(0, value.addOptions(List.of())); + assertEquals(List.of("a", "b", "c"), value.getOptions()); + } + + @Test + void jsonRoundTripIgnoresUnknownOptions() { + ModeValue value = new ModeValue("m", "a", "a", "b"); + + value.fromJson("b"); + assertEquals("b", value.toJson()); + + value.fromJson("nope"); + value.fromJson(42); + value.fromJson(null); + assertEquals("b", value.get()); + } + + @Test + void optionsListIsImmutable() { + ModeValue value = new ModeValue("m", "a", "a", "b"); + + assertThrows(UnsupportedOperationException.class, () -> value.getOptions().add("c")); + } +} diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/config/values/PrimitiveValuesTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/config/values/PrimitiveValuesTest.java new file mode 100644 index 0000000..97806c7 --- /dev/null +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/config/values/PrimitiveValuesTest.java @@ -0,0 +1,182 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.config.values; + +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class PrimitiveValuesTest { + + @Test + void numberClampsDefaultToRange() { + assertEquals(3, new NumberValue<>("n", 5, 0, 3).get()); + assertEquals(0, new NumberValue<>("n", -5, 0, 3).get()); + } + + @Test + void numberPreservesDeclaredType() { + assertInstanceOf(Double.class, new NumberValue<>("n", 1.5, 0.0, 3.0).get()); + assertInstanceOf(Float.class, new NumberValue<>("n", 0.5f, 0.0f, 3.0f).get()); + assertInstanceOf(Long.class, new NumberValue<>("n", 2L, 0L, 3L).get()); + assertInstanceOf(Integer.class, new NumberValue<>("n", 2, 0, 3).get()); + } + + @Test + void numberToJsonRoundsFractionsToTwoDecimals() { + assertEquals(1.23, new NumberValue<>("n", 1.23456, 0.0, 5.0).toJson()); + assertEquals(7.0, new NumberValue<>("n", 7, 0, 9).toJson()); + } + + @Test + void numberFromJsonClampsAndIgnoresForeignTypes() { + NumberValue<Integer> value = new NumberValue<>("n", 1, 0, 3); + + value.fromJson(99); + assertEquals(3, value.get()); + + value.fromJson("99"); + value.fromJson(null); + assertEquals(3, value.get()); + } + + @Test + void numberExposesBounds() { + NumberValue<Integer> value = new NumberValue<>("n", 1, 0, 3); + assertEquals(0, value.getMin()); + assertEquals(3, value.getMax()); + } + + @Test + void booleanRoundTripIgnoresForeignTypes() { + BooleanValue value = new BooleanValue("b", true); + + assertEquals(true, value.toJson()); + value.fromJson(false); + assertFalse(value.get()); + + value.fromJson("false"); + value.fromJson(null); + assertFalse(value.get()); + } + + @Test + void stringRoundTripIgnoresForeignTypes() { + StringValue value = new StringValue("s", "default"); + + assertEquals("default", value.toJson()); + value.fromJson("next"); + assertEquals("next", value.get()); + + value.fromJson(42); + assertEquals("next", value.get()); + } + + @Test + void setDefendsAgainstExternalMutation() { + SetValue value = new SetValue("set", Set.of("a", "b")); + + Set<String> input = new LinkedHashSet<>(); + input.add("x"); + value.set(input); + input.add("y"); + + assertEquals(Set.of("x"), value.get()); + + value.set(null); + assertTrue(value.get().isEmpty()); + } + + @Test + void setFromJsonFiltersNonStrings() { + SetValue value = new SetValue("set"); + + List<Object> json = new ArrayList<>(); + json.add("a"); + json.add(1); + json.add(null); + json.add("b"); + value.fromJson(json); + + assertEquals(new LinkedHashSet<>(List.of("a", "b")), value.get()); + assertEquals(List.of("a", "b"), value.toJson()); + } + + @Test + void setFromJsonNonListKeepsCurrent() { + SetValue value = new SetValue("set", Set.of("keep")); + value.fromJson("nope"); + assertEquals(Set.of("keep"), value.get()); + } + + @Test + void itemIdSetNormalizesNamespace() { + ItemIdSetValue value = new ItemIdSetValue("ids", Set.of("STONE", "minecraft:Dirt", " ", "")); + + assertEquals(new LinkedHashSet<>(List.of("minecraft:stone", "minecraft:dirt")), value.get()); + } + + @Test + void itemIdSetAddsDefaultNamespace() { + ItemIdSetValue value = new ItemIdSetValue("ids"); + value.set(Set.of("diamond_sword")); + + assertEquals(Set.of("minecraft:diamond_sword"), value.get()); + } + + @Test + void booleanMapSeedsDefaultsAndDefaultsUnknownToFalse() { + Map<String, Boolean> defaults = new LinkedHashMap<>(); + defaults.put("alpha", true); + defaults.put("beta", false); + BooleanMapValue value = new BooleanMapValue("map", defaults); + + assertTrue(value.get("alpha")); + assertFalse(value.get("beta")); + assertFalse(value.get("unknown")); + } + + @Test + void booleanMapFromJsonAppliesOnlyKnownKeys() { + Map<String, Boolean> defaults = new LinkedHashMap<>(); + defaults.put("alpha", true); + BooleanMapValue value = new BooleanMapValue("map", defaults); + + Map<String, Object> json = new LinkedHashMap<>(); + json.put("alpha", false); + json.put("unknown", true); + json.put("num", 5); + value.fromJson(json); + + assertFalse(value.get("alpha")); + assertFalse(value.getAll().containsKey("unknown")); + assertEquals(Map.of("alpha", false), value.toJson()); + } + + @Test + void booleanMapFromJsonNonMapResetsToDefaults() { + Map<String, Boolean> defaults = new LinkedHashMap<>(); + defaults.put("alpha", true); + BooleanMapValue value = new BooleanMapValue("map", defaults); + + value.set("alpha", false); + value.fromJson("garbage"); + + assertTrue(value.get("alpha")); + } +} diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/events/EventBusTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/events/EventBusTest.java new file mode 100644 index 0000000..12192c6 --- /dev/null +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/events/EventBusTest.java @@ -0,0 +1,233 @@ +/* + * 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; + +import dev.loki.lovisual.Lifecycle; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class EventBusTest { + + static class BaseEvent extends Event { + } + + static class DerivedEvent extends BaseEvent { + } + + @BeforeAll + static void activateLifecycle() { + // EventBus.post() доставляет события только при активном lifecycle. + Lifecycle.activate(); + } + + @Test + void postNullReturnsNull() { + EventBus bus = new EventBus(); + assertNull(bus.post(null)); + } + + @Test + void deliversToHandlerAndReturnsSameInstance() { + EventBus bus = new EventBus(); + SingleListener listener = new SingleListener(); + bus.register(listener); + + BaseEvent event = new BaseEvent(); + assertSame(event, bus.post(event)); + assertTrue(listener.called); + } + + @Test + void higherPriorityRunsFirst() { + EventBus bus = new EventBus(); + PriorityListener listener = new PriorityListener(); + bus.register(listener); + + bus.post(new BaseEvent()); + assertEquals(List.of("high", "low"), listener.calls); + } + + @Test + void subclassDispatchFiresDerivedThenBase() { + EventBus bus = new EventBus(); + HierarchyListener listener = new HierarchyListener(); + bus.register(listener); + + bus.post(new DerivedEvent()); + assertEquals(List.of("derived", "base"), listener.calls); + + listener.calls.clear(); + bus.post(new BaseEvent()); + assertEquals(List.of("base"), listener.calls); + } + + @Test + void unregisterStopsDelivery() { + EventBus bus = new EventBus(); + SingleListener listener = new SingleListener(); + bus.register(listener); + bus.unregister(listener); + + bus.post(new BaseEvent()); + assertFalse(listener.called); + assertFalse(bus.hasListeners(BaseEvent.class)); + } + + @Test + void doubleRegistrationIgnored() { + EventBus bus = new EventBus(); + SingleListener listener = new SingleListener(); + bus.register(listener); + bus.register(listener); + + assertEquals(1, bus.listenerCount()); + bus.post(new BaseEvent()); + assertTrue(listener.called); + } + + @Test + void listenerWithoutValidHandlersIsIgnored() { + EventBus bus = new EventBus(); + bus.register(new Object()); + bus.register(new InvalidSignaturesListener()); + + assertEquals(0, bus.listenerCount()); + assertFalse(bus.hasListeners(BaseEvent.class)); + } + + @Test + void handlerExceptionDoesNotStopOtherHandlers() { + EventBus bus = new EventBus(); + ThrowingListener throwing = new ThrowingListener(); + SingleListener healthy = new SingleListener(); + bus.register(throwing); + bus.register(healthy); + + assertDoesNotThrow(() -> bus.post(new BaseEvent())); + assertTrue(healthy.called); + } + + @Test + void privateHandlerInvoked() { + EventBus bus = new EventBus(); + PrivateListener listener = new PrivateListener(); + bus.register(listener); + + bus.post(new BaseEvent()); + assertTrue(listener.called); + } + + @Test + void hasListenersReflectsRegistration() { + EventBus bus = new EventBus(); + assertFalse(bus.hasListeners(BaseEvent.class)); + + SingleListener listener = new SingleListener(); + bus.register(listener); + assertTrue(bus.hasListeners(BaseEvent.class)); + assertTrue(bus.hasListeners(DerivedEvent.class)); + assertFalse(bus.hasListeners(Event.class)); + } + + @Test + void clearRemovesAllListeners() { + EventBus bus = new EventBus(); + SingleListener listener = new SingleListener(); + bus.register(listener); + bus.clear(); + + assertEquals(0, bus.listenerCount()); + assertFalse(bus.hasListeners(BaseEvent.class)); + bus.post(new BaseEvent()); + assertFalse(listener.called); + } + + @Test + void nullAndUnknownListenersAreNoOps() { + EventBus bus = new EventBus(); + assertDoesNotThrow(() -> { + bus.register(null); + bus.unregister(null); + bus.unregister(new SingleListener()); + }); + assertEquals(0, bus.listenerCount()); + } + + static class SingleListener { + boolean called; + + @EventHandler + public void onBase(BaseEvent event) { + called = true; + } + } + + static class PriorityListener { + final List<String> calls = new ArrayList<>(); + + @EventHandler(priority = 5) + public void onHigh(BaseEvent event) { + calls.add("high"); + } + + @EventHandler(priority = 1) + public void onLow(BaseEvent event) { + calls.add("low"); + } + } + + static class HierarchyListener { + final List<String> calls = new ArrayList<>(); + + @EventHandler + public void onBase(BaseEvent event) { + calls.add("base"); + } + + @EventHandler + public void onDerived(DerivedEvent event) { + calls.add("derived"); + } + } + + static class ThrowingListener { + @EventHandler + public void onBase(BaseEvent event) { + throw new IllegalStateException("boom"); + } + } + + static class PrivateListener { + boolean called; + + @EventHandler + private void onBase(BaseEvent event) { + called = true; + } + } + + static class InvalidSignaturesListener { + @EventHandler + public void twoParams(BaseEvent event, int extra) { + } + + @EventHandler + public void notAnEvent(String value) { + } + } +} diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/command/CommandDiscoveryTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/command/CommandDiscoveryTest.java index 9b9c375..194d59e 100644 --- a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/command/CommandDiscoveryTest.java +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/command/CommandDiscoveryTest.java @@ -44,8 +44,9 @@ class CommandDiscoveryTest { assertEquals("legacy", legacy.metadata().id()); assertEquals(List.of(), legacy.metadata().aliases()); - assertEquals("legacy", legacy.metadata().usage()); - assertEquals("legacy", legacy.metadata().description()); + // Default usage is now prefixed with the active command prefix. + assertEquals("%legacy", legacy.metadata().usage()); + assertEquals("%legacy", legacy.metadata().description()); } @SuppressWarnings("removal") diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/command/CommandPrefixTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/command/CommandPrefixTest.java new file mode 100644 index 0000000..f1e8ea3 --- /dev/null +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/command/CommandPrefixTest.java @@ -0,0 +1,49 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.command; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class CommandPrefixTest { + + @Test + void percentIsTheActivePrefix() { + assertEquals('%', CommandPrefix.CHAR); + assertTrue(CommandPrefix.isCommandLike("%help")); + assertTrue(CommandPrefix.isCommandLike(" %toggle keepinventory on")); + } + + @Test + void legacyAtIsStillAccepted() { + assertTrue(CommandPrefix.isCommandLike("@help")); + assertEquals('@', CommandPrefix.leadingPrefix("@help")); + } + + @Test + void plainChatIsNotACommand() { + assertFalse(CommandPrefix.isCommandLike("hello world")); + assertFalse(CommandPrefix.isCommandLike("/vanilla command")); + assertFalse(CommandPrefix.isCommandLike("")); + assertFalse(CommandPrefix.isCommandLike(null)); + assertEquals(0, CommandPrefix.leadingPrefix("50% off")); + } + + @Test + void stripRemovesPrefixAndWhitespace() { + assertEquals("help", CommandPrefix.strip(" %help ")); + assertEquals("toggle mod on", CommandPrefix.strip("@toggle mod on")); + assertEquals("plain text", CommandPrefix.strip("plain text")); + assertNull(CommandPrefix.strip(null)); + assertEquals("", CommandPrefix.strip(" ")); + } +} diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/command/UsageHintsTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/command/UsageHintsTest.java new file mode 100644 index 0000000..59f9484 --- /dev/null +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/command/UsageHintsTest.java @@ -0,0 +1,49 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.command; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +final class UsageHintsTest { + + @Test + void extractsPlaceholdersFromUsage() { + List<String> parts = UsageHints.placeholders("%toggle <module> [on|off|toggle]"); + assertEquals(List.of("module", "on|off|toggle"), parts); + } + + @Test + void placeholderAtPosition() { + String usage = "%friend [list|add|remove|toggle|clear] [player]"; + assertEquals("list|add|remove|toggle|clear", UsageHints.placeholder(usage, 1)); + assertEquals("player", UsageHints.placeholder(usage, 2)); + assertNull(UsageHints.placeholder(usage, 3)); + } + + @Test + void usageWithoutPlaceholdersYieldsNothing() { + assertNull(UsageHints.placeholder("%ping", 1)); + assertNull(UsageHints.placeholder(null, 1)); + assertNull(UsageHints.placeholder("", 1)); + } + + @Test + void unclosedPlaceholderIsStillFound() { + assertEquals(List.of("module"), UsageHints.placeholders("%bind <module")); + } + + @Test + void mixedAngleAndSquareBrackets() { + assertEquals(List.of("a", "b"), UsageHints.placeholders("%x <a> [b]")); + } +} diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/misc/NoSoundTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/misc/NoSoundTest.java new file mode 100644 index 0000000..4e0247d --- /dev/null +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/misc/NoSoundTest.java @@ -0,0 +1,34 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.misc; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +final class NoSoundTest { + + @Test + void resolveScaleConvertsPercent() { + assertEquals(0.3f, NoSound.resolveScale(true, 30), 0.0001f); + assertEquals(1.0f, NoSound.resolveScale(true, 100), 0.0001f); + assertEquals(0.01f, NoSound.resolveScale(true, 1), 0.0001f); + } + + @Test + void unmatchedSoundKeepsOriginalVolume() { + assertEquals(1.0f, NoSound.resolveScale(false, 30), 0.0001f); + assertEquals(1.0f, NoSound.resolveScale(false, 100), 0.0001f); + } + + @Test + void scaleIsClampedToSaneRange() { + assertEquals(1.0f, NoSound.resolveScale(true, 250), 0.0001f); + assertEquals(0.0f, NoSound.resolveScale(true, -5), 0.0001f); + } +} diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/misc/ScrollMovePolicyTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/misc/ScrollMovePolicyTest.java new file mode 100644 index 0000000..ce812a6 --- /dev/null +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/misc/ScrollMovePolicyTest.java @@ -0,0 +1,60 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.misc; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class ScrollMovePolicyTest { + + @Test + void tinyScrollIsIgnored() { + assertFalse(ScrollMovePolicy.isScrollSignificant(0.0)); + assertFalse(ScrollMovePolicy.isScrollSignificant(0.01)); + assertTrue(ScrollMovePolicy.isScrollSignificant(1.0)); + assertTrue(ScrollMovePolicy.isScrollSignificant(-1.0)); + } + + @Test + void directionMatchesSign() { + assertEquals(1, ScrollMovePolicy.direction(2.0)); + assertEquals(-1, ScrollMovePolicy.direction(-0.5)); + } + + @Test + void hoveredSlotWinsOverHotbarFallback() { + assertEquals(17, ScrollMovePolicy.resolveTargetSlot(true, 17, 3, 36)); + } + + @Test + void hotbarFallbackMapsSelectedSlotIntoMenu() { + // Standard inventory menu: 36 slots, hotbar = last 9 (indices 27..35). + assertEquals(27, ScrollMovePolicy.resolveTargetSlot(false, -1, 0, 36)); + assertEquals(30, ScrollMovePolicy.resolveTargetSlot(false, -1, 3, 36)); + assertEquals(35, ScrollMovePolicy.resolveTargetSlot(false, -1, 8, 36)); + } + + @Test + void hotbarIndexWrapsNegativeSlots() { + assertEquals(35, ScrollMovePolicy.hotbarIndexInMenu(-1, 36)); + } + + @Test + void emptyMenuYieldsNoSlot() { + assertEquals(-1, ScrollMovePolicy.hotbarIndexInMenu(0, 0)); + assertEquals(-1, ScrollMovePolicy.hotbarIndexInMenu(0, -5)); + } + + @Test + void quickMoveUsesLeftClickButton() { + assertEquals(0, ScrollMovePolicy.quickMoveButton()); + } +} diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/AutoAcceptTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/AutoAcceptTest.java new file mode 100644 index 0000000..d05e19e --- /dev/null +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/AutoAcceptTest.java @@ -0,0 +1,74 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.player; + +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +final class AutoAcceptTest { + + private static final Set<String> PATTERNS = Set.of("просит телепортироваться к себе", "has requested to teleport"); + + @Test + void extractsSenderBeforeRussianPattern() { + String sender = AutoAccept.TeleportRequestParser.extractSender( + "Steve123 просит телепортироваться к себе", PATTERNS); + assertEquals("Steve123", sender); + } + + @Test + void extractsSenderBeforeEnglishPattern() { + String sender = AutoAccept.TeleportRequestParser.extractSender( + "Herobrine has requested to teleport to you", PATTERNS); + assertEquals("Herobrine", sender); + } + + @Test + void ignoresDecorationsBeforeNickname() { + String sender = AutoAccept.TeleportRequestParser.extractSender( + "» [Клан] Night_Wolf просит телепортироваться к себе", PATTERNS); + assertEquals("Night_Wolf", sender); + } + + @Test + void unrelatedMessageYieldsNull() { + assertNull(AutoAccept.TeleportRequestParser.extractSender("Player joined the game", PATTERNS)); + } + + @Test + void blankOrNullInputsYieldNull() { + assertNull(AutoAccept.TeleportRequestParser.extractSender(null, PATTERNS)); + assertNull(AutoAccept.TeleportRequestParser.extractSender("", PATTERNS)); + assertNull(AutoAccept.TeleportRequestParser.extractSender("Steve просит телепортироваться к себе", Set.of())); + assertNull(AutoAccept.TeleportRequestParser.extractSender("Steve просит телепортироваться к себе", null)); + } + + @Test + void patternAtStartOfMessageYieldsNull() { + // No nickname-like token before the pattern means no sender. + assertNull(AutoAccept.TeleportRequestParser.extractSender("просит телепортироваться к себе", PATTERNS)); + } + + @Test + void earliestPatternWins() { + Set<String> both = Set.of("has requested", "просит телепортироваться к себе"); + String sender = AutoAccept.TeleportRequestParser.extractSender( + "Alex has requested to teleport (просит телепортироваться к себе)", both); + assertEquals("Alex", sender); + } + + @Test + void shortTokensAreNotNicknames() { + assertNull(AutoAccept.TeleportRequestParser.lastNicknameIn("ab – ")); + assertEquals("abc", AutoAccept.TeleportRequestParser.lastNicknameIn("abc – ")); + } +} diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/AutoLeaveTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/AutoLeaveTest.java new file mode 100644 index 0000000..898a47f --- /dev/null +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/AutoLeaveTest.java @@ -0,0 +1,63 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.player; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class AutoLeaveTest { + + @Test + void zeroDelayLeavesImmediatelyOnTriggerEdge() { + AutoLeave.LeavePolicy policy = new AutoLeave.LeavePolicy(); + assertTrue(policy.onTick(true, 1000L, 0L)); + } + + @Test + void delayWaitsBeforeLeaving() { + AutoLeave.LeavePolicy policy = new AutoLeave.LeavePolicy(); + assertFalse(policy.onTick(true, 1000L, 2000L)); + assertFalse(policy.onTick(true, 2500L, 2000L)); + assertTrue(policy.onTick(true, 3001L, 2000L)); + } + + @Test + void quietTickRearmsTheGate() { + AutoLeave.LeavePolicy policy = new AutoLeave.LeavePolicy(); + assertFalse(policy.onTick(true, 1000L, 1000L)); + assertFalse(policy.onTick(false, 1500L, 1000L)); + assertFalse(policy.onTick(true, 2000L, 1000L)); + assertTrue(policy.onTick(true, 3000L, 1000L)); + } + + @Test + void warningIsConsumedOnce() { + AutoLeave.LeavePolicy policy = new AutoLeave.LeavePolicy(); + assertTrue(policy.consumeWarning()); + assertFalse(policy.consumeWarning()); + policy.reset(); + assertTrue(policy.consumeWarning()); + } + + @Test + void healthThresholdIsInclusive() { + assertTrue(AutoLeave.LeavePolicy.isHealthLow(6.0f, 6.0f)); + assertTrue(AutoLeave.LeavePolicy.isHealthLow(5.5f, 6.0f)); + assertFalse(AutoLeave.LeavePolicy.isHealthLow(6.5f, 6.0f)); + } + + @Test + void playerCountRequiresAtLeastOne() { + assertTrue(AutoLeave.LeavePolicy.isPlayerCountReached(1, 1)); + assertTrue(AutoLeave.LeavePolicy.isPlayerCountReached(3, 2)); + assertFalse(AutoLeave.LeavePolicy.isPlayerCountReached(1, 2)); + assertFalse(AutoLeave.LeavePolicy.isPlayerCountReached(0, 1)); + } +} diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/UseTrackerTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/UseTrackerTest.java new file mode 100644 index 0000000..24858f0 --- /dev/null +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/player/UseTrackerTest.java @@ -0,0 +1,38 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.features.module.modules.player; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class UseTrackerTest { + + @Test + void defaultOptionsTrackInvisibilityOnly() { + var options = UseTracker.UseEffects.defaultOptions(); + assertEquals(6, options.size()); + assertTrue(options.get("invisibility")); + assertEquals(false, options.get("speed")); + assertEquals(false, options.get("strength")); + } + + @Test + void totemPopEventIdMatchesVanilla() { + // Entity event 35 is the vanilla totem-of-undying activation event. + assertEquals(35, UseTracker.TOTEM_POP_EVENT_ID); + } + + @Test + void knownKeysResolveToHumanNames() { + assertEquals("Invisibility", UseTracker.UseEffects.displayName("invisibility")); + assertEquals("Fire Resistance", UseTracker.UseEffects.displayName("fire_resistance")); + assertEquals("an effect", UseTracker.UseEffects.displayName("unknown")); + } +} diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/ChinaHatModelTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/ChinaHatModelTest.java new file mode 100644 index 0000000..6f507b8 --- /dev/null +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/ChinaHatModelTest.java @@ -0,0 +1,83 @@ +/* + * 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; + +import dev.loki.lovisual.render.helpers.HeadAnchor; +import net.minecraft.world.phys.Vec3; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; + +final class ChinaHatModelTest { + + private static final double EPS = 1e-3; + + private static HeadAnchor atOrigin() { + return HeadAnchor.world(Vec3.ZERO); + } + + @Test + void apexSitsOnTopOfCone() { + ChinaHatModel.Layout layout = ChinaHatModel.layout(0.5f, 0.22f, 24, 0f, 0f, 0f, atOrigin()); + + assertEquals(0.0, layout.apex().x, EPS); + assertEquals(0.22, layout.apex().y, EPS); + assertEquals(0.0, layout.apex().z, EPS); + } + + @Test + void rimRingIsFlatAtBaseRadius() { + int segments = 24; + float radius = 0.5f; + ChinaHatModel.Layout layout = ChinaHatModel.layout(radius, 0.22f, segments, 0f, 0f, 0f, atOrigin()); + + assertEquals(segments, layout.rim().length); + for (Vec3 rim : layout.rim()) { + assertEquals(0.0, rim.y, EPS); + double distanceFromAxis = Math.sqrt(rim.x * rim.x + rim.z * rim.z); + assertEquals(radius, distanceFromAxis, EPS); + } + } + + @Test + void rimVerticesAreEvenlySpaced() { + int segments = 4; + ChinaHatModel.Layout layout = ChinaHatModel.layout(1.0f, 0.22f, segments, 0f, 0f, 0f, atOrigin()); + + for (int i = 0; i < segments; i++) { + int next = (i + 1) % segments; + double side = layout.rim()[i].distanceTo(layout.rim()[next]); + assertEquals(Math.sqrt(2.0), side, EPS); + } + } + + @Test + void yawRotationSpinsRimAroundApex() { + float radius = 0.5f; + ChinaHatModel.Layout before = ChinaHatModel.layout(radius, 0.22f, 24, 0f, 0f, 0f, atOrigin()); + ChinaHatModel.Layout after = ChinaHatModel.layout(radius, 0.22f, 24, 90f, 0f, 0f, atOrigin()); + + assertEquals(0.22, after.apex().y, EPS); + assertEquals(before.rim()[0].distanceTo(after.apex()), + after.rim()[6].distanceTo(after.apex()), 0.15); + } + + @Test + void zeroSegmentsProducesEmptyRim() { + ChinaHatModel.Layout layout = ChinaHatModel.layout(0.5f, 0.22f, 0, 0f, 0f, 0f, atOrigin()); + + assertEquals(0, layout.rim().length); + } + + @Test + void buildIsNoOpWithoutMesh() { + assertDoesNotThrow(() -> ChinaHatModel.build(null, null, atOrigin(), 40, 0.5f, 0.22f, + 0f, 0f, 0f, (i, l, a) -> 0, (i, l, a) -> 0, 1.0f)); + } +} diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/LightTintTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/LightTintTest.java new file mode 100644 index 0000000..1231190 --- /dev/null +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/features/module/modules/visuals/LightTintTest.java @@ -0,0 +1,75 @@ +/* + * 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; + +import org.joml.Vector3f; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +final class LightTintTest { + + private static final float EPS = 1e-4f; + + @Test + void toLinearMapsBytesToUnitRange() { + Vector3f red = Light.toLinear(0xFF0000); + assertEquals(1.0f, red.x, EPS); + assertEquals(0.0f, red.y, EPS); + assertEquals(0.0f, red.z, EPS); + } + + @Test + void toLinearIgnoresAlpha() { + Vector3f a = Light.toLinear(0x00FF0000); + Vector3f b = Light.toLinear(0xAAFF0000); + assertEquals(a.x, b.x, EPS); + assertEquals(a.y, b.y, EPS); + assertEquals(a.z, b.z, EPS); + } + + @Test + void tintMixZeroKeepsCurrent() { + Vector3f target = Light.toLinear(0xFF0000); + Vector3f out = Light.tintAmbient(new Vector3f(0.2f, 0.3f, 0.4f), target, 0.0f); + assertEquals(0.2f, out.x, EPS); + assertEquals(0.3f, out.y, EPS); + assertEquals(0.4f, out.z, EPS); + } + + @Test + void tintMixOneEqualsTarget() { + Vector3f target = Light.toLinear(0x102030); + Vector3f out = Light.tintAmbient(new Vector3f(0.9f, 0.9f, 0.9f), target, 1.0f); + assertEquals(target.x, out.x, EPS); + assertEquals(target.y, out.y, EPS); + assertEquals(target.z, out.z, EPS); + } + + @Test + void tintMixHalfIsAverage() { + Vector3f target = Light.toLinear(0x000000); + Vector3f out = Light.tintAmbient(new Vector3f(0.8f, 0.8f, 0.8f), target, 0.5f); + assertEquals(0.4f, out.x, EPS); + } + + @Test + void nullCurrentBehavesAsWhite() { + Vector3f out = Light.tintAmbient(null, new Vector3f(0.0f, 0.0f, 0.0f), 1.0f); + assertEquals(0.0f, out.x, EPS); + } + + @Test + void mixClampedToUnitRange() { + Vector3f target = Light.toLinear(0xFF0000); + Vector3f high = Light.tintAmbient(new Vector3f(0.1f, 0.2f, 0.3f), target, 5.0f); + assertEquals(target.x, high.x, EPS); + Vector3f low = Light.tintAmbient(new Vector3f(0.1f, 0.2f, 0.3f), target, -1.0f); + assertEquals(0.1f, low.x, EPS); + } +} diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/combat/ReachMeterTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/combat/ReachMeterTest.java new file mode 100644 index 0000000..84a13fe --- /dev/null +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/combat/ReachMeterTest.java @@ -0,0 +1,85 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.util.combat; + +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +final class ReachMeterTest { + + private static final double EPS = 1e-6; + + @BeforeEach + void reset() { + ReachMeter.reset(); + } + + @Test + void distanceToBoxOutsidePoint() { + AABB box = new AABB(0, 0, 0, 1, 1, 1); + + assertEquals(2.0, ReachMeter.distanceToBox(new Vec3(3, 0.5, 0.5), box), EPS); + assertEquals(Math.sqrt(3.0), ReachMeter.distanceToBox(new Vec3(2, 2, 2), box), EPS); + } + + @Test + void distanceToBoxInsideIsZero() { + AABB box = new AABB(0, 0, 0, 1, 1, 1); + + assertEquals(0.0, ReachMeter.distanceToBox(new Vec3(0.5, 0.5, 0.5), box), EPS); + assertEquals(0.0, ReachMeter.distanceToBox(new Vec3(0, 1, 0.5), box), EPS); + } + + @Test + void distanceToBoxHandlesNulls() { + assertEquals(-1.0, ReachMeter.distanceToBox(null, new AABB(0, 0, 0, 1, 1, 1)), EPS); + assertEquals(-1.0, ReachMeter.distanceToBox(Vec3.ZERO, null), EPS); + } + + @Test + void recordTracksLastAndMax() { + ReachMeter.record(2.5, 1000L); + ReachMeter.record(3.4, 2000L); + ReachMeter.record(1.2, 3000L); + + assertEquals(1.2, ReachMeter.lastDistance(), EPS); + assertEquals(3.4, ReachMeter.sessionMax(), EPS); + assertEquals(3000L, ReachMeter.lastAttackAtMs()); + } + + @Test + void invalidDistancesIgnored() { + ReachMeter.record(2.5, 1000L); + ReachMeter.record(Double.NaN, 2000L); + ReachMeter.record(-5.0, 3000L); + + assertEquals(2.5, ReachMeter.lastDistance(), EPS); + assertEquals(1000L, ReachMeter.lastAttackAtMs()); + } + + @Test + void resetClearsState() { + ReachMeter.record(4.0, 1000L); + ReachMeter.reset(); + + assertEquals(-1.0, ReachMeter.lastDistance(), EPS); + assertEquals(-1.0, ReachMeter.sessionMax(), EPS); + assertEquals(0L, ReachMeter.lastAttackAtMs()); + } + + @Test + void formatMeters() { + assertEquals("-", ReachMeter.format(-1.0)); + assertEquals("3.00 m", ReachMeter.format(3.0)); + assertEquals("3.12 m", ReachMeter.format(3.124)); + } +} diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/text/LegacyTextUtilTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/text/LegacyTextUtilTest.java new file mode 100644 index 0000000..ac9009b --- /dev/null +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/text/LegacyTextUtilTest.java @@ -0,0 +1,165 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.util.text; + +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.Style; +import net.minecraft.network.chat.TextColor; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +/** + * convertLegacyCodes строит дерево: Component.empty() → сегмент-обёртка → стилизованные листья. + * firstLeaf() спускается к первому листу с текстом. + */ +final class LegacyTextUtilTest { + + private static Component firstLeaf(Component in) { + Component cur = in; + while (!cur.getSiblings().isEmpty()) { + cur = cur.getSiblings().get(0); + } + return cur; + } + + /** Все листовые компоненты с непустым текстом, в порядке обхода. */ + private static java.util.List<Component> leaves(Component in) { + java.util.List<Component> out = new java.util.ArrayList<>(); + collectLeaves(in, out); + return out; + } + + private static void collectLeaves(Component in, java.util.List<Component> out) { + if (in.getSiblings().isEmpty()) { + if (!in.getString().isEmpty()) out.add(in); + return; + } + for (Component child : in.getSiblings()) { + collectLeaves(child, out); + } + } + + @Test + void ampersandToSectionConvertsColorCodes() { + assertEquals("§aGreen", LegacyTextUtil.ampersandToSection("&aGreen")); + assertEquals("plain text", LegacyTextUtil.ampersandToSection("plain text")); + } + + @Test + void ampersandToSectionConvertsHexNotation() { + assertEquals("§x§F§F§0§0§0§0X", LegacyTextUtil.ampersandToSection("&#FF0000X")); + assertEquals("§x§0§1§2§3§4§5", LegacyTextUtil.ampersandToSection("&x&0&1&2&3&4&5")); + } + + @Test + void ampersandToSectionKeepsLoneAmpersands() { + assertEquals("at & end", LegacyTextUtil.ampersandToSection("at & end")); + } + + @Test + void ampersandToSectionHandlesNullAndEmpty() { + assertNull(LegacyTextUtil.ampersandToSection(null)); + assertEquals("", LegacyTextUtil.ampersandToSection("")); + } + + @Test + void stripLegacyRemovesColorAndDecorationCodes() { + assertEquals("Green", LegacyTextUtil.stripLegacy("§aGreen")); + assertEquals("RedBold", LegacyTextUtil.stripLegacy("&cRed§lBold")); + assertEquals("Hex", LegacyTextUtil.stripLegacy("§x§F§F§0§0§0§0Hex")); + assertEquals("", LegacyTextUtil.stripLegacy(null)); + assertEquals("no codes", LegacyTextUtil.stripLegacy("no codes")); + } + + @Test + void convertLegacyCodesReturnsSameInstanceWithoutCodes() { + Component in = Component.literal("plain"); + assertSame(in, LegacyTextUtil.convertLegacyCodes(in)); + } + + @Test + void convertLegacyCodesHandlesNull() { + assertEquals("", LegacyTextUtil.convertLegacyCodes(null).getString()); + } + + @Test + void convertLegacyCodesAppliesVanillaColor() { + Component out = LegacyTextUtil.convertLegacyCodes(Component.literal("&aHi")); + + assertEquals("Hi", out.getString()); + assertEquals(TextColor.fromRgb(0x55FF55), firstLeaf(out).getStyle().getColor()); + } + + @Test + void convertLegacyCodesAppliesHexColor() { + Component out = LegacyTextUtil.convertLegacyCodes(Component.literal("§x§F§F§0§0§0§0Hex")); + + assertEquals("Hex", out.getString()); + assertEquals(TextColor.fromRgb(0xFF0000), firstLeaf(out).getStyle().getColor()); + } + + @Test + void convertLegacyCodesAppliesBedrockMinecoinGold() { + Component out = LegacyTextUtil.convertLegacyCodes(Component.literal("§gCoin")); + + assertEquals("Coin", out.getString()); + assertEquals(TextColor.fromRgb(0xDDD605), firstLeaf(out).getStyle().getColor()); + } + + @Test + void convertLegacyCodesKeepsBaseStyle() { + Style base = Style.EMPTY.withBold(true); + Component in = Component.literal("§aHi").setStyle(base); + + Component out = LegacyTextUtil.convertLegacyCodes(in); + Style leafStyle = firstLeaf(out).getStyle(); + + assertEquals(TextColor.fromRgb(0x55FF55), leafStyle.getColor()); + assertEquals(Boolean.TRUE, leafStyle.isBold()); + } + + @Test + void convertLegacyCodesSplitsMultiStyleSegments() { + Component out = LegacyTextUtil.convertLegacyCodes(Component.literal("&aRed&lBold")); + + assertEquals("RedBold", out.getString()); + } + + @Test + void colorCodeResetsDecorationsToBase() { + // парсер трактует цвет по-ваналлийски: код цвета возвращает стиль к base + цвет, + // более ранние decoration-коды из строки сбрасываются (селектор isColor должен их распознавать) + Component out = LegacyTextUtil.convertLegacyCodes(Component.literal("§lBold§aGreen")); + + assertEquals("BoldGreen", out.getString()); + var leafList = leaves(out); + assertEquals(2, leafList.size()); + + Style boldStyle = leafList.get(0).getStyle(); + assertEquals(Boolean.TRUE, boldStyle.isBold()); + + Style greenStyle = leafList.get(1).getStyle(); + assertEquals(TextColor.fromRgb(0x55FF55), greenStyle.getColor()); + assertEquals(Boolean.FALSE, greenStyle.isBold()); + } + + @Test + void decorationCodeKeepsColorAndAddsFlag() { + Component out = LegacyTextUtil.convertLegacyCodes(Component.literal("&aRed&lBold")); + + var leafList = leaves(out); + assertEquals(2, leafList.size()); + Style boldStyle = leafList.get(1).getStyle(); + assertEquals(Boolean.TRUE, boldStyle.isBold()); + assertEquals(TextColor.fromRgb(0x55FF55), boldStyle.getColor()); + } +} diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/text/NameMaskerTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/text/NameMaskerTest.java new file mode 100644 index 0000000..aea9a3a --- /dev/null +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/text/NameMaskerTest.java @@ -0,0 +1,84 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.util.text; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +final class NameMaskerTest { + + @Test + void masksNicknameTokenDeterministically() { + NameMasker masker = new NameMasker("Player", List.of()); + String first = masker.mask("gg Notch_123 gg"); + String second = masker.mask("gg Notch_123 gg"); + + assertNotEquals("gg Notch_123 gg", first); + assertEquals(first, second); + assertEquals("gg Player1 gg", first); + } + + @Test + void differentNamesGetDifferentOrdinals() { + NameMasker masker = new NameMasker("Player", List.of()); + String masked = masker.mask("Notch and Herobrine"); + + assertEquals("Player1 and Player2", masked); + } + + @Test + void shortTokensAndNumbersAreKept() { + NameMasker masker = new NameMasker("Player", List.of()); + + assertEquals("ab 12345 x9", masker.mask("ab 12345 x9")); + } + + @Test + void wordsInsideTokensAreNotSplit() { + NameMasker masker = new NameMasker("Player", List.of()); + + // The nickname-like token wins over word matching because it is longer. + String masked = masker.mask("Steve2016"); + assertEquals("Player1", masked); + } + + @Test + void serverWordsAreMaskedCaseInsensitively() { + NameMasker masker = new NameMasker("Server", List.of("anarchy")); + + assertEquals("Welcome to Server-2!", masker.mask("Welcome to Anarchy-2!")); + assertEquals("Server", masker.mask("ANARCHY")); + } + + @Test + void wordMatchTakesPriorityOverTokenMasking() { + NameMasker masker = new NameMasker("Server", List.of("anarchy")); + + // "anarchy" matches as a word first, so masking stops there. + assertEquals("Server", masker.mask("anarchy")); + } + + @Test + void nullAndEmptyPassThrough() { + NameMasker masker = new NameMasker("Player", List.of()); + + assertEquals(null, masker.mask(null)); + assertEquals("", masker.mask("")); + } + + @Test + void blankReplacementFallsBackToPlayer() { + NameMasker masker = new NameMasker(" ", List.of()); + + assertEquals("Player1", masker.mask("Notch")); + } +} diff --git a/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/text/NameProtectorTest.java b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/text/NameProtectorTest.java new file mode 100644 index 0000000..7550c5e --- /dev/null +++ b/combatant-client-26.2/src/test/java/dev/loki/lovisual/util/text/NameProtectorTest.java @@ -0,0 +1,116 @@ +/* + * This file is part of the LoVisual Client distribution. + * Copyright (c) 2026 loki5512344. + * + * Licensed under the GNU General Public License v3.0. + */ + +package dev.loki.lovisual.util.text; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class NameProtectorTest { + + @Test + void replacesNameCaseInsensitively() { + NameProtector p = new NameProtector(); + p.put("Notch", "Steve"); + + assertEquals("hi Steve!", p.replace("hi NOTCH!")); + } + + @Test + void onlyMatchesWholeWords() { + NameProtector p = new NameProtector(); + p.put("Notch", "Steve"); + + assertEquals("Notchless is a biome-like word", p.replace("Notchless is a biome-like word")); + assertEquals("xNotch Notchy", p.replace("xNotch Notchy")); + } + + @Test + void longerNamesWinOverSubstrings() { + NameProtector p = new NameProtector(); + p.put("Cool", "X"); + p.put("CoolGuy", "Y"); + + assertEquals("I am Y", p.replace("I am CoolGuy")); + assertEquals("X beans", p.replace("Cool beans")); + } + + @Test + void underscoresCountAsWordCharacters() { + NameProtector p = new NameProtector(); + p.put("lob", "cat"); + + // "lobo" и "_lob_" не матчатся (word-символы вокруг), isolated "lob" — матчится + assertEquals("lobo_here _lob_ cat", p.replace("lobo_here _lob_ lob")); + } + + @Test + void multipleNamesInOneString() { + NameProtector p = new NameProtector(); + p.put("Alice", "**"); + p.put("Bob", "**"); + + assertEquals("** and **", p.replace("Alice and Bob")); + } + + @Test + void blankNamesIgnoredAndEmptyWhenNone() { + NameProtector p = new NameProtector(); + p.put(null, "x"); + p.put(" ", "x"); + + assertTrue(p.isEmpty()); + assertEquals("anything", p.replace("anything")); + } + + @Test + void emptyAliasDeletesName() { + NameProtector p = new NameProtector(); + p.put("Notch", ""); + + assertEquals("hi !", p.replace("hi Notch!")); + } + + @Test + void nullAndEmptyTextPassThrough() { + NameProtector p = new NameProtector(); + p.put("Notch", "X"); + + assertEquals(null, p.replace(null)); + assertEquals("", p.replace("")); + } + + @Test + void exactReplacementOnlyForWholeString() { + NameProtector p = new NameProtector(); + p.put("Notch", "X"); + + assertEquals("X", p.replaceExact("Notch")); + assertEquals("Notchy", p.replaceExact("Notchy")); + } + + @Test + void clearRemovesAllAliases() { + NameProtector p = new NameProtector(); + p.put("Notch", "X"); + p.clear(); + + assertTrue(p.isEmpty()); + assertEquals("Notch", p.replace("Notch")); + } + + @Test + void duplicateNameOverwritesAlias() { + NameProtector p = new NameProtector(); + p.put("Notch", "X"); + p.put("Notch", "Y"); + + assertEquals("Y", p.replace("Notch")); + } +}