chore(history): squash 13 commit(s) from 2026-09-08

- fix: replace field-level Minecraft.getInstance() with lazy locals — Phase 5 sweep complete
- docs: plan Phase 7 — new modules inspired by SoupVisuals (re-implementation, no code copied)
- test: Phase 6 — EventBus, config values, profile codec/diff, LegacyTextUtil (54 new tests)
- fix: LegacyTextUtil.isColor via explicit EnumSet instead of ChatFormatting.ordinal()
- fix: last 4 field-level Minecraft instances (ESP, NameTags, TargetHud, ViewModel) — true end of Phase 5
- fix: ClickPearl + ElytraRecastUtil 'client' fields -> lazy access (final mc-field leftovers)
- feat: Phase 7 — Light, Hitboxes, NameProtect, Watermark (re-implemented from SoupVisuals ideas)
- fix: ChinaHat geometry — proper shallow cone per SoupVisuals profile
- refactor: Watermark — static UI element in our style, not a Soup port
- fix: startup crash and trollface texture errors from in-game log
- docs+feat: DEVELOPMENT.md guide, Watermark username field, README refresh
- feat: Reach HUD widget (Settings -> UI) — last hit distance, pure info
- feat: Phase 7 Delta QoL modules + command prefix % with full autocomplete
This commit is contained in:
loki5512344 2026-09-08 23:46:29 +02:00
parent e1d7d19a83
commit 707820e418
167 changed files with 5034 additions and 345 deletions

43
TODO.md
View file

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

View file

@ -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-<ver>.jar
./gradlew test # юнит-тесты (сейчас ~320)
./gradlew runClient # быстрый dev-запуск Minecraft с модом
./gradlew runClient --rerun-tasks # если кэш врёт
```
Готовый jar для теста в реальной сборке — скопировать в инстанс:
```bash
cp build/libs/lovisual-*.jar /storage/prismlauncher/instances/26.2/minecraft/mods/
```
Ресурсы (текстуры) — **только настоящие PNG** (сигнатура `89 50 4E 47`).
JPEG, переименованный в .png, уронит `Bad PNG Signature` при загрузке ресурспака.
---
## 2. Правила проекта (нарушение = откат)
- **KISS / DRY / SOLID** — простая структура, без оверинжиниринга.
- **≤ 200 строк на файл.** Фасад-делегат (например `Renderer2D`) — исключение.
Больше логики — выноси package-private хелперы в тот же пакет.
- **≤ 4 файла на папку** — больше, дели папку по смыслу (подпапка).
- **Никакого статики от Minecraft**: `Minecraft.getInstance()` — только локальной
переменной в месте использования, НИКОГДА в поле (`private final Minecraft mc = ...` — баг).
Функциональный синглтон (`INSTANCE`) — только для сервисов без состояния жизненного цикла.
- **`Lifecycle.isActive()/canRunHud()` — гейты** для рендера/событий; `EventBus.post()`
сам игнорирует события до activation.
- **Никаких this-escape** — конструктор не публикует `this`.
- **Никаких мёртвых миксинов** — миксин только если реально нужен;
`cancellable=true` только при реальном cancel.
- **`dev.loki.lovisual.mixins.*` — ТОЛЬКО классы с `@Mixin`.** Обычный helper-класс
внутри mixin-пакета крашит игру: Knot не даёт transformed-классу обратиться к нему
(`IllegalClassLoadError`). Хелперы — в `util/` или пакет фичи (см. `HandDynamics`).
- **Без пустышек** — файл без реализации не создаётся.
- **Каждый этап компилируется**: `./gradlew build` перед коммитом.
- **Тесты обязательны** для новой чистой логики (JUnit 5, без моков Minecraft —
выноси математику/парсинг в чистые статические методы и тесть их).
- **Референсы (`ref/`)** — чужой код (SoupVisuals SOUP-1.0, Delta и др.):
**только идеи**, пере-реализация под наши правила, никакого копирования.
---
## 3. Карта пакетов
```
dev.loki.lovisual
├── LoVisual.java — инициализация ClientModInitializer
├── Lifecycle.java — INIT → ACTIVE → SHUTDOWN, гейты canRunHud/Render
├── events/ — EventBus (@EventHandler, приоритеты, гейт по Module)
│ └── impl/ — конкретные события (Tick, RenderHud, Packet, Lightmap...)
├── config/ — MainConfig, SettingDef, сериализация (.lvcfg, legacy .cbcfg)
│ ├── values/ — ConfigValue: bool/num/mode/color/set/... (есть тесты)
│ └── profile/ — профили конфигов (collect/apply/diff/binary codec)
├── features/
│ ├── module/ — Module, ModuleCategory, ModuleInfo, ModuleManager,
│ │ └── modules/... — сами модули по категориям
│ ├── gui/ — ClickGUI, HUD (draggable + nondraggable), BetterChat
│ ├── command/ — чат-команды (@CommandInfo, ClassGraph-поиск)
│ ├── relations/ — друзья/враги/стаф (PlayerRelations, CategoryService)
│ └── security/ — AntiBot-фильтр, StaffTracker
├── render/engine/ — Renderer2D/3D, пайплайны, шрифты, MeshBuilder
├── mixins/ — @Mixin-классы (см. правило выше)
└── util/ — чистые утилиты (text/animation/time/...) — тут живут тесты
```
---
## 4. Как добавить МОДУЛЬ
Модули находятся в `features/module/modules/<категория>/`. Категории:
`COMBAT · MOVEMENT · PLAYER · VISUALS · MISC` (см. `ModuleCategory`).
Легит-направление: читерские модули (автоудары, полёты, speed...) **удаляем, не добавляем**.
Регистрация автоматическая: **ClassGraph сканирует `@ModuleInfo`** — ничего никуда
дописывать не надо. Пустой конструктор обязателен.
Шаблон:
```java
@ModuleInfo(id = "mymodule", displayName = "My Module", category = ModuleCategory.VISUALS)
public class MyModule extends Module {
private static final String SETTING_X = "x_amount";
private final NumberValue<Integer> amount = num("myModuleAmount", SETTING_X, 5, 1, 10);
@Override
public void onTick() {
Minecraft mc = Minecraft.getInstance(); // локально, не в поле!
if (mc.level == null) return;
// ...
}
@EventHandler
private void onRenderHud(RenderHudEvent event) { /* авто-подписка через EventBus */ }
}
```
Ключевые хуки `Module`: `onEnable/onDisable/onTick/onFrame`,
`onRenderHudEngine(...)`, `onRenderWorldEngine(...)`, `getWorldPhase()/getHudPhase()`.
Хелперы настроек сами объявляют `SettingDef`: `bool/num/mode/color/colorNoAlpha/
enumSetting/text/group/bind` + `visibleWhen(value, () -> cond)`.
Модуль-лист по умолчанию недоступен цели — смотри также `boolCommon`/`modeCommon`
для общих схем (`CommonSettingSchemas`).
Доступ к чужому модулю: `Modules.get(X.class)`, проверка состояния — `isEnabled()`.
Мировой рендер (боксы/линии) — через `Renderer3D.batch(WORLD_COLORED_LINES, DepthMode...)`
и MeshBuilder; готовые куски геометрии — `EspMeshBuilder.addOutlineBox` и т.п.
---
## 5. Как добавить HUD-элемент
Два вида, оба ищутся ClassGraph-ом автоматически:
**A. Draggable (перетаскиваемый, вкладка Settings → HUD):**
`features/gui/hud/draggable/impl/`, аннотация `@HudElementInfo(id, displayName,
enabledByDefault, order)`, базовый класс `DraggableHudElement`.
Обязательно: `applyDefaultPosition(screenW, screenH)`, рендер в `renderEngine(...)`,
`usesEngineRenderer() = true`. Позиция сохраняется через `layoutValue`.
**B. Static (фиксированный, вкладка Settings → UI):**
`features/gui/hud/nondraggable/impl/`, аннотация `@HudElementRegister(order=N)`,
базовый класс `AbstractHudElement`, конструктор `super(id, "Title", defaultEnabled)`,
синглтон `public static final X INSTANCE` (лоадер забирает его из статического поля).
Чтобы карточка появилась во вкладке **UI**, добавь `new StaticHudCard("<id>", "<Title>")`
в `STATIC_UI_CARDS` в `MenuSettingsResolver.java` (draggable-виджеты там не нужны —
они собираются из реестра сами).
Цвет из темы: `Theme.theme().accent()/windowBg()/textMuted()`; альфа —
`HudRenderUtil.setAlpha(argb, a)`; текст — `Fonts.renderer(name, type, fallback)`
(`begin(scale,...)` → `getWidth`/`render` → `end()`).
---
## 6. Как добавить СОБЫТИЕ / МИКСИН
1. Событие: наследуй `Event` (есть `cancel()`), положи в `events/impl/`.
2. Публикация: `Events.BUS.post(new MyEvent(...))` из миксина.
3. Миксин: класс с `@Mixin(Target.class)` в `mixins/` **и имя в `lovisual.mixins.json`**
(секции `mixins`/`client`). Миксин, которого нет в json, не применится.
4. Если миксин опциональный (зависит от чужого мода, напр. Sodium) — цель может
отсутствовать: Mixin напечатает WARN и пройдёт (см. `SodiumChunkMeshFormatsMixin`).
Такой WARN — норма, краш — нет.
5. Чистая логика из миксина — вобы в обычный класс вне `mixins/`-пакета (см. правило про `HandDynamics`).
---
## 7. Настройки, конфиг и i18n
- Конфиги модулей/HUD пишутся в `<game>/lovisual/` в формате **`.lvcfg`**
(legacy `.cbcfg` читается). Профили — `config/profile/` (кнопка в ClickGUI → Configs).
- Каждой настройке нужен i18n-ключ в **обоих** файлах
`src/main/resources/assets/lovisual/lang/{en_us,ru_ru}.json`:
- модуль: `module.<id>.description`, настройки `setting.<id>.<setting_id>`,
опции режима `setting.<id>.<setting_id>.option.<Option>`;
- draggable HUD: `hud.draggable.<id>` / `setting.draggable.<id>.<key>`;
- static HUD: `setting.hud_element.<id>.<key>` (см. Watermark, Swap Tooltip).
- `setting_id` — snake_case (`"show_ping"`); configName —camelCase с префиксом мода
(`"watermark_show_ping"`) — из него авто-выводится id.
- Пропущенный ключ ловит `MissingI18nReporter` в дев-окружении — проверяй консоль.
---
## 8. Тесты
`src/test/java/` зеркалит main. Пишутся **только на чистую логику** (без Minecraft-
рантайма): парсеры, математика, codec'ы, матчинги. Примеры:
- `util/text/NameProtectorTest`, `LegacyTextUtilTest` — парсинг/подмена,
- `config/values/*Test` — значения конфига,
- `config/profile/ConfigProfileValueCodecTest` — бинарный round-trip,
- `events/EventBusTest` — события (нужен `Lifecycle.activate()` в `@BeforeAll`).
Если фича завязана на Minecraft — выделяй статический pure-метод
(`static int tint(...)` / `static record Layout(...)`) и тесть его (см. `Light.tintAmbient`).
---
## 9. Чеклист перед коммитом
- [ ] `./gradlew build` зелёный, `./gradlew test` зелёный
- [ ] новых файлов >200 строк нет; папка не разрослась >4 файлов
- [ ] нет полей `Minecraft` / `getInstance()` в полях
- [ ] lang-ключи en_us + ru_ru добавлены для всех новых настроек
- [ ] тест на новую чистую логику есть
- [ ] в mixin-пакете только `@Mixin`-классы
- [ ] TODO.md обновлён (галка/заметка), если это этап плана
- [ ] в чужой код (`ref/`) не заглядывался копипаст

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 &lt;module&gt; [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: "&lt;module&gt;" becomes "%toggle &lt;module&gt;"
* 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);
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,68 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.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());
}}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

Some files were not shown because too many files have changed in this diff Show more