feat: реорганизация combat/visuals модулей + добавлен InvisESP

This commit is contained in:
loki5512344 2026-09-01 13:53:01 +02:00
parent ff42902c43
commit 4a529466c7
27 changed files with 1478 additions and 2602 deletions

View file

@ -0,0 +1,564 @@
# LoVisual Mod — Dev Guide / Справочник по созданию модулей
Цель: быстро посмотреть структуру и написать новый модуль / HUD-элемент /
команду, не перекапывая весь код. Всё ниже — сводка по реальному коду.
- Пакеты: `dev.loki.lovisual.*`
- Minecraft 26.2, Fabric, Java 25, Lombok
- Правила проекта (важно!): файл ≤200 строк, папка ≤3 файлов,
`./gradlew build` после каждого этапа, тесты для новой логики,
никаких `static final Minecraft` в полях — только ленивый `Minecraft.getInstance()`.
---
## 1. Архитектура (главные пакеты)
```
dev.loki.lovisual
├── LoVisual.java — ClientModInitializer: весь старт (тики, рендер-фазы)
├── Lifecycle.java — INIT → ACTIVE → SHUTDOWN; гейты isActive/canRunRender/canRunHud
├── features/
│ ├── module/ — СИСТЕМА МОДУЛЕЙ (главное для вас)
│ │ ├── Module.java — базовый класс (1181 стр, фасад)
│ │ ├── ModuleInfo — @annotation для модуля
│ │ ├── ModuleManager — реестр + диспетчер фаз
│ │ ├── ModuleAutoLoader — авто-обнаружение по аннотации (ClassGraph)
│ │ ├── Modules — удобный доступ: Modules.get(Класс.class)
│ │ ├── ModuleCategory — COMBAT/MOVEMENT/PLAYER/VISUALS/MISC
│ │ ├── HudPhase / WorldPhase — фазы рендера
│ │ └── modules/{combat,player,visuals,misc}/
│ ├── command/ — команды: @CommandInfo + ClientCommand
│ ├── gui/
│ │ ├── clickgui/ — экран настроек (Setting → виджет)
│ │ └── hud/ — HUD-элементы
│ │ ├── draggable/ — перетаскиваемые (Fps, Coords, ModuleList, TargetHud…)
│ │ └── nondraggable/ — статичные (CustomBar, CustomHotbar, BetterChat…)
│ ├── relations/ — друзья/враги/категории (CategoryService)
│ ├── security/ — BackdoorProtection (SSRF/translate-защита)
│ └── theme/ — темы: Theme.theme() → Themes.Theme (цвета)
├── config/
│ ├── values/ — типы настроек: ConfigValue (bool/num/mode/color…)
│ ├── SettingDef.java — GUI-нейтральный дескриптор настройки
│ └── common/CommonSettingSchemas — переиспользуемые схемы (i18n-ключи)
├── events/
│ ├── EventBus (Events.BUS) — шина событий
│ ├── @EventHandler — аннотация хендлера
│ └── impl/ — все события (см. §5)
├── mixins/ — миксины + accessors (mixins/accessors/)
├── addon/ — система аддонов (API v0)
├── api/v0/ — публичный API аддонов (module/client/render/clickgui)
├── render/
│ ├── engine/
│ │ ├── renderer/Renderer2D — весь 2D-рендер (прямоугольники, круги, текст, items)
│ │ ├── renderer/Renderer3D — 3D (линии, меши, quads)
│ │ ├── text/ — Fonts.renderer("Onest", Regular), TextRenderer
│ │ ├── animation/AnimationUtility
│ │ ├── pipeline/LoVisualRenderPipelines — готовые пайплайны (UI_COLORED и т.д.)
│ │ └── uniform/MeshBuilder — построение мешей
│ └── engine/color/RenderColor — ARGB
└── util/ — куча хелперов (см. §8)
```
---
## 2. Жизненный цикл и старт
В `LoVisual.onInitializeClient()` по порядку:
1. `LoVisualRenderEngineBootstrap.init()`
2. `MainConfig.get()`, `AccountConfig.get()`, `CommandManager.init()`
3. `ModuleAutoLoader.load("dev.loki.lovisual.features.module.modules")` — сканирует пакет,
ищет классы с `@ModuleInfo`, создаёт через no-args конструктор и зовёт `module.postInit()`.
4. `ModuleManager.loadAllModuleConfigs()` — применяет сохранённые конфиги, включает enabled-модули.
5. `HudElements.init()`, `StaticHudElementBootstrap.init()` — HUD-элементы.
6. `Events.BUS.register(...)` — реестрируются глобальные сервисы.
7. `Lifecycle.activate()` — после этого события/тики работают.
**Вывод:** чтобы добавить модуль — просто положи класс с `@ModuleInfo` в пакет
`...modules.<категория>` и всё. Реестрировать руками не нужно.
`Lifecycle` — главный гейт: `Lifecycle.isActive()`, `canRunRender()`, `canRunHud()`.
До `activate()` модули и события не работают.
---
## 3. Модуль — быстрый старт
Минимальный модуль (по образцу `FullBright`):
```java
package dev.loki.lovisual.features.module.modules.misc;
import dev.loki.lovisual.config.values.NumberValue;
import dev.loki.lovisual.events.EventHandler;
import dev.loki.lovisual.events.impl.GameTickEvent;
import dev.loki.lovisual.features.module.Module;
import dev.loki.lovisual.features.module.ModuleCategory;
import dev.loki.lovisual.features.module.ModuleInfo;
@ModuleInfo(
id = "my_feature",
displayName = "My Feature",
aliases = {"mf"},
category = ModuleCategory.MISC,
enabledByDefault = false
)
public class MyFeature extends Module {
private final NumberValue<Integer> amount =
num("myFeatureAmount", 3, 0, 20); // имя_конфига, дефолт, мин, макс
@Override
public void onEnable() {
// вызывается при включении
}
@Override
public void onDisable() {
}
@Override
public void onTick() {
// каждый игровой тик, если модуль включён
if (!isEnabled()) return;
int a = amount.get();
}
}
```
### `@ModuleInfo`
```java
id = "имя" // нижний регистр, уникальный, это ключ конфига и команды
displayName = "Имя" // показывается в UI; можно i18n-ключ
aliases = {} // необязательно
category = ModuleCategory.XXX
enabledByDefault = false
description = "" // или i18n-ключ "module.<id>.description"
```
### Автогенерируемые настройки модуля
У каждого модуля уже есть (создаются в конструкторе `Module`):
- `enabled` (BooleanValue) — состояние,
- `bind` (KeyBindSetting, значение `KeyBindValue`) — клавиша,
- `activation_source`,
- `show_in_module_list`.
`postInit()` вызывается автоматически лоадером: читает поля-настройки, делает
`SettingFactory.fromDefs(...)`, регистрирует модуль в `ModuleManager`.
### Хуки жизненного цикла (переопределяемые)
| Метод | Когда |
|-------|-------|
| `onEnable()` / `onDisable()` | переключение модуля |
| `onTick()` | каждый игровой тик (в мире) |
| `onFrame(float tickDelta)` | каждый кадр |
| `onKey(int key, int action)` | нажатия клавиш |
| `onRender2D(GuiGraphicsExtractor)` | vanilla HUD pass |
| `onRenderHudEngine(Renderer2D, TextRenderer, ...)` | кастомный 2D-рендер (см. §6) |
| `onRenderHudEngineForeground(...)` | 2D поверх всего |
| `onRenderWorld(PoseStack, SubmitNodeCollector)` | 3D vanilla-style |
| `onRenderWorldEngine(Renderer3D, Renderer3D, float)` | 3D через движок (см. §7) |
| `getHudPhase()` / `getWorldPhase()` | фаза, в которой рендерить |
> `getHudPhase()`/`getWorldPhase()` по умолчанию `NONE` — модуль вообще не рендерится
> диспетчером. Верните фазу, чтобы рендер-хуки вызывались (см. §6/§7).
### Управление состоянием
- `isEnabled()`, `setEnabled(boolean)`, `setEnabled(boolean, ModuleActivationSource)`, `toggle()`
- `isAvailable()` / `getAvailabilityReason()` — переопределите `getUnavailableReason()`
- `name()`, `getDisplayName()`, `getAliases()`, `getCategory()`, `getDescription()`
- `saveConfig()`, `getConfigValue(String)`, `getSettings()`
### Доступ к другим модулям
```java
import dev.loki.lovisual.features.module.Modules;
Freecam fc = Modules.get(Freecam.class); // null если не активен/не зарегистрирован
boolean on = Modules.enabled("freecam"); // по id
```
`ModuleManager.get(String id)` / `ModuleManager.get(Class)` / `ModuleManager.require(Class)` тоже доступны.
---
## 4. Настройки (ConfigValue → Setting)
Базовый класс: `config/values/ConfigValue<T>` — `get()`, `set(v)`, `getName()`,
`toJson()`, `fromJson(Object)`, `toDisplay()`.
Внутри модуля настройки создаются **хелперами** в `Module` (все `protected final`).
Имя в конфиге и `settingId` (i18n) могут различаться.
### Хелперы в Module / BaseHudElement
| Хелпер | Тип | Пример |
|--------|-----|--------|
| `bool(name, def)` | `BooleanValue` | `bool("my_on", true)` |
| `num(name, def, min, max)` | `NumberValue<Integer/Long/Float/Double>` | `num("my_num", 3, 0, 20)` |
| `mode(name, def, options...)` | `ModeValue` (String) | `mode("my_mode", "A", "A", "B", "C")` |
| `enumMode(name, defEnum)` / `enumSetting(name, defEnum, ...)` | `EnumValue<E>` | см. `Reach` |
| `color(name, "#AARRGGBB")` | `RGBAColorValue` | `color("my_bg", "#F7343434")` |
| `colorNoAlpha(name, "#RRGGBB")` | `RGBColorValue` | `colorNoAlpha("my_fg", "#FFFFFF")` |
| `text(name, def)` | `StringValue` | `text("my_text", "")` |
| `textList(name)` / `textList(name, pickerMode)` | `SetValue` (множество строк) | `textList("my_list")` |
| `itemList(name)` | `ItemIdSetValue` (список предметов) | `itemList("my_items")` |
| `group(name, defaultsMap)` | `BooleanMapValue` (группа чекбоксов) | `group("gui_sounds", Map.of(...))` |
| `bind(name, def, mode)` | `KeyBindValue` | `bind("my_bind", "R", BindMode.PRESS)` |
| `action(name, defaultKey, mode)` | `FunctionBindSetting` | см. ниже |
| `setDefaultBind("KEY")` | — | клавиша модуля (см. `ClickGui`) |
**Варианты с общим i18n** (`*Common`): `boolCommon`, `numCommon`, `modeCommon`,
`enumCommon`, `color` (без common) — берут схему из `CommonSettingSchemas`:
```java
distance = numCommon("reachDistance", "range", CommonSettingSchemas.COMBAT_RANGE, 4.5, 3.0, 6.0);
mode = enumCommon("reachMode", "mode", CommonSettingSchemas.ANTICHEAT_MODE, Mode.NORMAL, Mode.values());
```
### Условная видимость / доступность
```java
private final NumberValue<Double> wallDistance =
visibleWhen(numCommon("reachWallDistance", "wall_range", schema, 3.0, 0.0, 6.0),
() -> raycast.get() == RaycastMode.THROUGH_WALLS);
// недоступен (показывается серым + причина):
appliesWhen(mySetting, () -> mc.player != null, "Только в мире");
```
### Экшены (вторая кнопка на модуль)
```java
action("climb", "G", BindMode.PRESS); // создаёт FunctionBindSetting
// в тике:
if (isActionPressedOnce("climb")) { ... } // фронт нажатия
if (isActionHeld("climb")) { ... } // удержание
```
### Настройки в HUD-элементах
У `BaseHudElement` те же хелперы (`bool`, `num`, `mode`, `enumSetting`, `color`,
`textList`, `bind`, `group`, `visibleWhen`), но объявление идёт в
`defineSettings(List<SettingDef>)` (см. §9).
### Виджеты в UI (SettingDef)
`SettingDef` — GUI-нейтральный дескриптор. `SettingFactory.fromDef(def)` превращает
его в виджет. Доступные `Kind`: `BOOLEAN, NUMBER, MODE, COLOR, COLOR_NO_ALPHA,
TEXT, TEXT_LIST, COOLDOWN_RULES, PROTOCOL_HEURISTICS, GROUP, BIND`.
Для модулей SettingDef строятся автоматически из хелперов — руками не нужно.
`@DisableSettingI18n(name = false, options = true)` — отключить i18n у настройки.
---
## 5. События (EventBus)
`Events.BUS.post(event)` — доставка хендлерам. Хендлер — любой метод с одним
аргументом-наследником `Event`, помеченный `@EventHandler(priority = N)`.
Подписчики регистрируются автоматически при `ModuleManager.register()` (модули),
или явно `Events.BUS.register(obj)` (в `LoVisual.onInitializeClient`).
Модули-подписчики автоматически гейтятся: если модуль выключен — его хендлеры не вызываются.
Поэтому в хендлерах модуля проверка `if (!isEnabled()) return;` не обязательна, но
встречается в коде для ясности.
`Event` — база с `cancelled`/`cancel()`. Наследование событий поддерживается.
Полный список `events/impl/` (по имени файла):
```
AttackEntityEvent BlinkPacketEvent CombatProtocolBossbarEvent
CrosshairTargetUpdateEvent EventBreakBlock EventCollision EventPostSync
EventPushOutOfBlocks EventSync EventTargetChanged FireworkEvent GameTickEvent
I18nPreflightCollectEvent KeybindIsPressedEvent KeyInputEvent LightmapEvent
LightmapModifyEvent MovementInputEvent PacketEvent PlayerJumpEvent PlayerMoveEvent
PlayerSafeWalkEvent PlayerStepEvent PlayerStepSuccessEvent PlayerVelocityStrafe
PostPlayerUpdateEvent PvpChatEvent PvpOverlayEvent PvpTabEvent
RenderPrewarmCollectEvent RotationUpdateEvent SprintControlEvent
```
Типичные события для нового модуля:
- `GameTickEvent` — каждый тик,
- `PacketEvent` — сетевые пакеты,
- `AttackEntityEvent`, `EventTargetChanged` — бой/цели,
- `LightmapModifyEvent` — свет (пример `FullBright`),
- `PlayerMoveEvent`, `MovementInputEvent` — движение,
- `RenderPrewarmCollectEvent` — «прогреть» шрифты/текстуры для вашего рендера.
Пример:
```java
@EventHandler
private void onTick(GameTickEvent event) { ... }
```
---
## 6. 2D-рендер (HUD)
### Фазы
Верните не-`NONE` фазу из `getHudPhase()`:
```java
HudPhase: FIRST, BEFORE_MISC_OVERLAYS, AFTER_MISC_OVERLAYS, AFTER_BOSS_BAR,
BEFORE_DEMO_TIMER, BEFORE_CHAT, AFTER_SUBTITLES, LAST
```
`ModuleManager.renderHudEngine(phase, renderer, textRenderer, ctx, tickDelta)` вызывает
у модуля `onRenderHudEngine(...)`; `renderHudEngineForeground` → `onRenderHudEngineForeground`.
### Renderer2D — главный 2D API
Включается/выключается через `begin()` / `render()` (уже делает диспетчер).
Готовые методы (фасад `Renderer2D.java`, все в `px`):
- **Фигуры:** `quad(x,y,w,h,argb)`, `quad(x,y,w,h,cTL,cTR,cBR,cBL)`,
`roundedRect(x,y,w,h,radius,argb)`, `roundedRectStroke(...)`,
`roundedRectGradient(...)`, `roundedRectGlow/Shadow/SoftShadow(...)`,
`circle(cx,cy,r,argb)`, `circleStroke(...)`, `arcStroke(...)`,
`line(x1,y1,x2,y2,argb)`, `boxLines(...)`, `roundedSmokeFill(...)`.
- **Текст:** см. §6.2.
- **Предметы:** `item(ItemStack, x, y, scale)`, `itemUnscaled(...)`, `itemPivot(...)`.
- **SVG/текстуры:** `svg(Identifier/Path, x, y, w, h)`, `textureQuad(...)`, `msdfTextureQuad(...)`.
- **Effect (стеклянные панели):** `effect(UiEffectSpec)`.
### Текст
```java
TextRenderer tr = Fonts.renderer("Onest", FontInfo.Type.Regular, textRenderer);
// или Fonts.renderer("OnestMedium", Regular), "OnestBold", "Inter", "Iosevka", "Icons"...
tr.begin(scale, true, false);
double w = tr.getWidth("text", false);
double h = tr.getHeight(false);
tr.render("text", x, y, new RenderColor(0xFFFFFFFF), false);
tr.end();
```
Универсальная запись «в одном блоке»:
```java
textRenderer.begin(scale, true, false);
textRenderer.render("FPS", x, y, new RenderColor(argb), false);
textRenderer.end();
```
`TextRenderer.get()` — текущий рендерер (передаётся в хук). `HudScale.scale(screenW, screenH)`
даёт масштаб для адаптивного HUD (см. `Fps`).
### Иконки / текстуры
Идентификаторы: `Identifier.fromNamespaceAndPath("lovisual", "textures/hud/elements/fps.png")`.
SVG лежат в `assets/lovisual/svg/`.
### Анимации
`AnimationUtility`: `approach(value, target, dt, speed)`, `fast(...)`, `lerp(...)`,
`easeOutCubic`, `easeInOutCubic`, `easeOutBack`, `blink(ms)`, `deltaTime()`, `snap(...)`.
### Хелперы
- `HudRenderUtil`: `setAlpha(argb, a)`, `mixColor(a, b, t)`, `glassBackground()`,
`drawLiquidGlass(...)`, `animateVisibility(...)`, `visibilityScale(...)`.
- `HudGlobalConfig.get()`: `getFontSize()`, `getBlurRadius()` и т.д.
- Тема: `Theme.theme()` (статический импорт `dev.loki.lovisual.features.theme.Theme.theme`)
→ `Themes.Theme` c полями `windowBg, windowHeader, windowStroke, surface, surfaceHover,
cardEnabled, cardDisabled, textPrimary, textMuted, accent, accentSoft, strokeSoft`
(всё `int` ARGB). См. `Fps.updatePalette()`.
---
## 7. 3D-рендер (мир)
### Фазы
Верните не-`NONE` фазу из `getWorldPhase()`:
```java
WorldPhase: BEFORE_ENTITIES, AFTER_ENTITIES, BEFORE_TRANSLUCENT, END_MAIN, AFTER_POST_PROCESS
```
### Renderer3D — простой путь
В хуке `onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta)`:
```java
renderer.begin();
renderer.line(x1, y1, z1, x2, y2, z2, r, g, b, a); // линия
renderer.quad(x1,y1,z1, x2,y2,z2, x3,y3,z3, x4,y4,z4); // квад
renderer.triangle(...);
renderer.render(new PoseStack()); // в конце
```
Пример: `Tracers.onRenderWorldEngine` (линии от камеры к игрокам).
### MeshBuilder + RenderPipeline — продвинутый путь
```java
MeshBuilder mesh = new MeshBuilder(LoVisualRenderPipelines.UI_TEXTURED_ADDITIVE);
mesh.begin();
mesh.ensureQuadCapacity();
int i1 = mesh.vec2(x, y).vec2(u, v).color(r, g, b, a).next();
int i2 = mesh.vec2(...).vec2(...).color(...).next();
mesh.quad(i1, i2, i3, i4);
mesh.end();
```
Готовые пайплайны в `LoVisualRenderPipelines` (фрагмент):
```
UI_COLORED, UI_COLORED_LINES, UI_TEXTURED, UI_TEXTURED_ADDITIVE, UI_TEXT, UI_TEXT_MSDF,
UI_SVG_MSDF, UI_BLUR, WORLD_COLORED, WORLD_COLORED_LINES, WORLD_COLORED_DEPTH,
WORLD_TEXTURED, WORLD_TEXTURED_DEPTH, WORLD_TEXT, WORLD_TEXT_MSDF, WORLD_DECAL_SDF,
WORLD_COLORED_LINES_DEPTH, WORLD_WIDE_COLORED_LINES, ...HAND_*, SHADER_ESP_*, POST_FX...
```
Отправить на отрисовку (2D-стрелки из `Tracers`):
```java
MeshRenderer.begin()
.attachments(mc.gameRenderer.mainRenderTarget())
.pipeline(LoVisualRenderPipelines.UI_TEXTURED_ADDITIVE)
.mesh(mesh).transform(matrix4f)
.sampler("u_Texture", view, sampler)
.end();
```
`Renderer3D.Cull` — утилиты отсечения: `isInFrustum(...)`, `isSectionVisible(...)`, `isInFront(...)`.
---
## 8. Полезные утилиты (`util/`)
| Пакет / класс | Что даёт |
|---------------|----------|
| `util.target.TargetManager` | текущая цель: `getTarget()`, `getTarget(true/false)`, `onAttack()`, `setPredictionTarget()`, `Source` |
| `util.pvp.PvpTracker`, `PvpTargetTracker` | статистика PvP / отслеживание цели |
| `util.aiming.RotationManager` | плавные/снап-ротации |
| `util.combat.VulcanReachController` | клампинг дистанции (режим античита) |
| `util.time.GameClock` | `millis()` — игровое время (тики×50мс), вне мира — стенное |
| `util.time.TimerController` | таймеры |
| `util.input.KeyManager` | `wasPressed("func")`, `isHeld("func")`, `isComboHeldAllowScreen(...)` |
| `util.screen.ClientScreen` | текущий экран: `ClientScreen.current(mc)` / `.show(mc, screen)` |
| `util.text` | текст-хелперы |
| `util.entity.simulation.PlayerSimulationCache`, `BoatSimulationCache` | предсказание позиций |
| `util.raycast` | рейкасты |
| `util.item.OmniItemUtils` | утилиты предметов |
| `util.combat.protocol.CombatProtocolHeuristics` | эвристики античита |
| `util.logging.DebugLog` | `DebugLog.info/warn/error/config(...)` (printf-style) |
| `util.resources.RenderResourceReadiness` | готовность рендер-ресурсов |
| `features.relations.CategoryService` | `isFriend(Player)`, `getColor(Player)` — категории игроков |
| `util.FastFps` | `getFps()` — быстрый FPS без накладок |
Ленивый доступ к MC:
```java
private final Minecraft mc = Minecraft.getInstance();
```
(поле, но это ок — поле инициализируется при создании модуля, а модули создаются
в рантайме; правило «никакого `static final Minecraft` в полях»).
---
## 9. HUD-элементы
### Draggable (перетаскиваемый) — пример `Fps`
```java
@HudElementInfo(id = "fps", displayName = "FPS", enabledByDefault = true, order = 120)
public final class Fps extends DraggableHudElement {
// настройки — прямо поля:
private final NumberValue<Double> scale = num("fps_scale", 2.37, 0.5, 5.0);
private final RGBColorValue iconColor = visibleWhen(colorNoAlpha("fps_icon_color", "#FFFFFF"), this::isCustomMode);
@Override
public void applyDefaultPosition(int screenW, int screenH) { this.x = 16f; this.y = 20f; }
@Override
public boolean usesEngineRenderer() { return true; }
@Override
public void renderEngine(Renderer2D renderer, TextRenderer textRenderer,
GuiGraphicsExtractor ctx, float tickDelta,
int screenW, int screenH) {
if (!preview && !isEnabled()) { width = 0f; height = 0f; return; }
// считаем width/height, рисуем через renderer + Fonts.renderer(...)
}
}
```
- Настройки объявляются как поля с хелперами (автоматом попадают в SettingDef).
- `defaultLayout(x, y, anchorX, anchorY)` / `defaultLinkedLayout(...)` — стартовое положение.
- `x, y, width, height` — позиция и размер (обновляйте width/height в renderEngine!).
- Регистрация автоматическая (сканирование по `@HudElementInfo`).
- Интерактив: `isMouseOverInteractive(mx, my)`, `onMouseClicked(mx, my, button)`.
### Nondraggable (статичный)
Классы в `features/gui/hud/nondraggable/impl/` (CustomBar, CustomHealthBar, CustomHotbar,
BetterTooltips, BetterButtons, DynamicIsland). Наследуют `BaseHudElement`, рендер через
`renderEngine(...)` + `getRenderSpace()`/`getHudPhase()`.
---
## 10. Команды
Интерфейс `ClientCommand` + аннотация `@CommandInfo`, регистрация — вручную в `CommandManager`.
```java
@CommandInfo(
id = "toggle",
aliases = "t",
usage = "@toggle <module> [on|off|toggle]",
descriptionKey = "command.toggle.description"
)
public final class ToggleCommand implements ClientCommand {
@Override
public boolean execute(CommandContext ctx) {
// ctx.arg(0), ctx.arg(1), ...
CommandOutput.success("Done");
return true;
}
@Override
public List<String> suggest(CommandContext ctx, int argIndex, String token) {
return List.of();
}
}
```
- `CommandContext(mc, raw, name, args)` — `arg(i)` возвращает null если нет.
- `CommandOutput` — `success/warning/error/send(String)` с префиксом `[LoVisual]`.
- `CommandManager.init()` регистрирует команды из `impl/` (через ClassGraph, как модули).
---
## 11. API для аддонов (`api/v0/`)
- `LoVisualAddon` — entrypoint `"lovisual:addon"`: `onConfigureModules`,
`onInitialize(LoVisualAddonContext)`, `onClientReady`, `onShutdown`.
- `LoVisualModuleExtension` — расширение модуля (`ModuleExtensionContext`),
перехватывает `beforeEnable/afterEnable/beforeTick/...` (см. `ModuleExtensionManager`).
- `LoVisualClientApi.get()` — `modules()`, `module(id)`, `isModuleEnabled(id)`, `setModuleEnabled`.
- `render/` — `LoVisualRenderCallback` + `LoVisualRenderStage`
(HUD_RAW, HUD_SCALED, HUD_LOGICAL, WORLD_*, SCREEN_*), `LoVisualPostProcessCallback`,
`LoVisualUniforms`, `LoVisualRenderPipelineBuilder`.
- `clickgui/` — `LoVisualClickGuiSection`, `LoVisualClickGuiRenderContext` — свои секции в кликгую.
---
## 12. i18n (языковые ключи)
Файлы: `src/main/resources/assets/lovisual/lang/en_us.json`, `ru_ru.json`.
Соглашения по ключам:
- Модуль: `module.<id>`, `module.<id>.name`, `module.<id>.description`,
настройка: `module.<id>.setting.<setting_id>`, опции: `module.<id>.option.<option>`.
- HUD draggable: `hud.draggable.<id>` / `setting.draggable.<id>.<setting>`.
- HUD static: `hud.static.<id>`.
- Команды: `command.<id>.description`.
- Общие схемы: `CommonSettingSchemas` указывают `commonI18nKeys` (например `combat.range`)
— они ищутся в lang-файлах с префиксом `setting.common.<key>` (проверьте по `CommonKey`).
`I18n.get("key")` — получение перевода. Если ключа нет — возвращается сам ключ.
В `Module.getDisplayName()`/`getDescription()` fallback на `displayName`/`description`.
---
## 13. Миксины и accessors
- Все миксины в `mixins/` (`@Mixin(...)`). Пакеты: `accessors`, `iris`, `sodium`, `moreculling`,
`xaero`, `security`.
- Accessors в `mixins/accessors/` — примеры: `MinecraftAccessor`, `LocalPlayerAccessor`,
`PlayerInventoryAccessor`, `LivingEntityAccessor`, `EntityAccessor`, `ItemCooldownManagerAccessor`.
- Реестр: `resources/lovisual.mixins.json`. `LoVisualMixinPlugin` — условная загрузка.
- Правило: «никаких мёртвых миксинов» — добавляйте миксин только если реально нужен.
- `mixininterface/` — интерфейсы-протуберанцы для доступа к данным из миксинов (`IGuiGraphics`, `IEntity`).
---
## 14. Чек-лист нового модуля
1. `@ModuleInfo` на классе в `features/module/modules/<категория>/`.
2. Унаследовать `Module`, переопределить `onEnable/onDisable/onTick` и т.д.
3. Настройки — поля с хелперами (`num`, `bool`, `mode`, `color`, `visibleWhen`…).
4. Если нужен рендер — вернуть `getHudPhase()`/`getWorldPhase()` и реализовать
`onRenderHudEngine`/`onRenderWorldEngine`.
5. Если нужны события — методы с `@EventHandler`.
6. `i18n`: добавить `module.<id>.name/description` и ключи настроек в `en_us.json`/`ru_ru.json`.
7. Проверки: `./gradlew build` (сборка), `./gradlew test` (тесты), для чистой логики — JUnit.
8. Файл ≤200 строк — иначе вынести логику в хелпер-классы в том же пакете.
**Не делайте:**
- `static final Minecraft` в полях → только ленивый `Minecraft.getInstance()`.
- `System.currentTimeMillis()` в модулях → `GameClock.millis()`.
- this-escape в конструкторе, `#[allow(dead_code)]`-аналогов (мёртвый код удаляется),
мёртвые миксины.

View file

@ -11,6 +11,7 @@ import dev.loki.lovisual.features.command.CommandOutput;
import dev.loki.lovisual.render.engine.svg.SvgRenderOptions; import dev.loki.lovisual.render.engine.svg.SvgRenderOptions;
import dev.loki.lovisual.render.engine.text.TextGlyphFallback; import dev.loki.lovisual.render.engine.text.TextGlyphFallback;
import dev.loki.lovisual.render.engine.text.TextRenderer; import dev.loki.lovisual.render.engine.text.TextRenderer;
import dev.loki.lovisual.util.time.GameClock;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import java.util.List; import java.util.List;
@ -129,13 +130,21 @@ final class BetterChatGlyphRenderer {
tr.render(text.toString(), runX - shadowOffset, textY, BetterChatRenderer.TMP_COLOR, false); tr.render(text.toString(), runX - shadowOffset, textY, BetterChatRenderer.TMP_COLOR, false);
tr.render(text.toString(), runX, textY + shadowOffset, BetterChatRenderer.TMP_COLOR, false); tr.render(text.toString(), runX, textY + shadowOffset, BetterChatRenderer.TMP_COLOR, false);
tr.render(text.toString(), runX, textY - shadowOffset, BetterChatRenderer.TMP_COLOR, false); tr.render(text.toString(), runX, textY - shadowOffset, BetterChatRenderer.TMP_COLOR, false);
long now = GameClock.millis();
float flow = (now % 2600L) / 2600.0f;
float breath = 0.5f + 0.5f * (float) Math.sin(now / 5200.0 * Math.PI * 2.0);
tr.renderQuadGradient(text.toString(), runX, textY, (idx, cp, x0, y0, x1, y1, out) -> { tr.renderQuadGradient(text.toString(), runX, textY, (idx, cp, x0, y0, x1, y1, out) -> {
float t0 = (float) ((x0 - runX) / runW); float t0 = (float) ((x0 - runX) / runW) + flow;
float t1 = (float) ((x1 - runX) / runW); float t1 = (float) ((x1 - runX) / runW) + flow;
int leftTop = BetterChatDrawUtil.mulAlpha(CommandOutput.mixRgb(start, end, t0), alpha); if (t0 > 1.0f) t0 -= 1.0f;
int rightTop = BetterChatDrawUtil.mulAlpha(CommandOutput.mixRgb(start, end, t1), alpha); if (t1 > 1.0f) t1 -= 1.0f;
int leftBottom = BetterChatDrawUtil.mulAlpha(CommandOutput.mixRgb(start, bottomMix, t0), alpha); int base0 = CommandOutput.mixRgb(start, end, t0);
int rightBottom = BetterChatDrawUtil.mulAlpha(CommandOutput.mixRgb(start, bottomMix, t1), alpha); int base1 = CommandOutput.mixRgb(start, end, t1);
int leftTop = BetterChatDrawUtil.mulAlpha(mixBreath(base0, breath), alpha);
int rightTop = BetterChatDrawUtil.mulAlpha(mixBreath(base1, breath), alpha);
int leftBottom = BetterChatDrawUtil.mulAlpha(mixBreath(CommandOutput.mixRgb(start, bottomMix, t0), breath), alpha);
int rightBottom = BetterChatDrawUtil.mulAlpha(mixBreath(CommandOutput.mixRgb(start, bottomMix, t1), breath), alpha);
out[0] = leftTop; out[0] = leftTop;
out[1] = leftBottom; out[1] = leftBottom;
out[2] = rightBottom; out[2] = rightBottom;
@ -145,4 +154,8 @@ final class BetterChatGlyphRenderer {
tr.end(); tr.end();
} }
} }
private static int mixBreath(int rgb, float breath) {
return CommandOutput.mixRgb(rgb, 0xFFFFFF, 0.06f * breath);
}
} }

View file

@ -0,0 +1,88 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.gui.chat;
import com.mojang.brigadier.suggestion.Suggestion;
import dev.loki.lovisual.features.command.CommandManager;
import dev.loki.lovisual.mixins.accessors.ChatScreenAccessor;
import dev.loki.lovisual.util.screen.ClientScreen;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.components.EditBox;
import net.minecraft.client.gui.screens.ChatScreen;
import net.minecraft.client.input.KeyEvent;
import java.util.List;
/**
* Tab / arrow-key cycling through the custom {@code @}-command suggestion popup.
* Vanilla CommandSuggestions only drives suggestions for {@code /} commands, so our
* {@code @} popup needs its own keyboard handling: Up/Down move the highlight, Tab applies.
*/
public final class BetterChatTabHandler {
private BetterChatTabHandler() {
}
private static String lastInput = "";
private static int lastSelection = -1;
private static int lastTotal = 0;
public static boolean handleKey(KeyEvent input) {
if (input == null) return false;
boolean down = input.isDown();
boolean up = input.isUp();
if (!down && !up && !input.isCycleFocus()) return false;
Minecraft mc = Minecraft.getInstance();
if (!(ClientScreen.current() instanceof ChatScreen chatScreen)) return false;
EditBox field = ((ChatScreenAccessor) chatScreen).getChatField();
if (field == null) return false;
String text = field.getValue();
if (text == null || text.isEmpty()) return false;
if (BetterChatSuggestionState.commandPrefix(text) != '@') return false;
int cursor = field.getCursorPosition();
List<Suggestion> suggestions = CommandManager.suggest(text, cursor);
if (suggestions == null || suggestions.isEmpty()) return false;
int total = suggestions.size();
if (!text.equals(lastInput) || lastTotal != total) {
lastInput = text;
lastTotal = total;
lastSelection = -1;
}
if (down || up) {
int delta = down ? 1 : -1;
int selection = lastSelection < 0 ? (delta > 0 ? 0 : total - 1) : (lastSelection + delta) % total;
if (selection < 0) selection += total;
lastSelection = selection;
trackSelection(suggestions, selection);
return true;
}
int selection = lastSelection < 0 ? 0 : (lastSelection + (input.hasShiftDown() ? -1 : 1)) % total;
if (selection < 0) selection += total;
lastSelection = selection;
Suggestion suggestion = suggestions.get(selection);
try {
field.setValue(suggestion.apply(text));
BetterChatEditBoxUtil.setCursorSafe(field, field.getValue().length());
trackSelection(suggestions, selection);
} catch (Exception ignored) {
return false;
}
return true;
}
private static void trackSelection(List<Suggestion> suggestions, int selection) {
BetterChatRenderer.suggestEntries = suggestions;
BetterChatRenderer.suggestCurrentSelection = selection;
BetterChatRenderer.suggestCurrentStart = 0;
BetterChatRenderer.lastSnapshot = null;
}
}

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0. * Licensed under the GNU General Public License v3.0.
*/ */
package dev.loki.lovisual.features.module.modules.visuals; package dev.loki.lovisual.features.module.modules.combat;
import dev.loki.lovisual.util.time.GameClock; import dev.loki.lovisual.util.time.GameClock;
@ -24,7 +24,7 @@ import dev.loki.lovisual.render.engine.renderer.FullScreenRenderer;
import dev.loki.lovisual.render.engine.uniform.impl.PostProcessUniforms; import dev.loki.lovisual.render.engine.uniform.impl.PostProcessUniforms;
//todo Description //todo Description
@ModuleInfo(id = "damagetint", displayName = "DamageTint", category = ModuleCategory.VISUALS) @ModuleInfo(id = "damagetint", displayName = "DamageTint", category = ModuleCategory.COMBAT)
public class DamageTint extends Module implements PostProcessPass { public class DamageTint extends Module implements PostProcessPass {
private static final String SETTING_START_HP = "start_hp"; private static final String SETTING_START_HP = "start_hp";

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0. * Licensed under the GNU General Public License v3.0.
*/ */
package dev.loki.lovisual.features.module.modules.visuals; package dev.loki.lovisual.features.module.modules.combat;
import com.mojang.blaze3d.pipeline.RenderPipeline; import com.mojang.blaze3d.pipeline.RenderPipeline;
import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.blaze3d.vertex.PoseStack;
@ -69,7 +69,7 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
//todo Description //todo Description
@ModuleInfo(id = "predictions", displayName = "Predictions", category = ModuleCategory.VISUALS) @ModuleInfo(id = "predictions", displayName = "Predictions", category = ModuleCategory.COMBAT)
public class Predictions extends Module { public class Predictions extends Module {
private static final String SETTING_LINE_COLOR = "line_color"; private static final String SETTING_LINE_COLOR = "line_color";
private static final String SETTING_PEARL_CUSTOM_COLOR = "pearl_custom_color"; private static final String SETTING_PEARL_CUSTOM_COLOR = "pearl_custom_color";

View file

@ -5,7 +5,7 @@
* Licensed under the GNU General Public License v3.0. * Licensed under the GNU General Public License v3.0.
*/ */
package dev.loki.lovisual.features.module.modules.visuals; package dev.loki.lovisual.features.module.modules.combat;
import dev.loki.lovisual.util.time.GameClock; import dev.loki.lovisual.util.time.GameClock;
@ -45,7 +45,7 @@ import java.util.List;
import java.util.function.IntFunction; import java.util.function.IntFunction;
//todo Description //todo Description
@ModuleInfo(id = "targetesp", displayName = "TargetESP", aliases = {"target", "targetrender"}, category = ModuleCategory.VISUALS) @ModuleInfo(id = "targetesp", displayName = "TargetESP", aliases = {"target", "targetrender"}, category = ModuleCategory.COMBAT)
public class TargetESP extends Module { public class TargetESP extends Module {
private static final String SETTING_MODE = "mode"; private static final String SETTING_MODE = "mode";

View file

@ -1,856 +0,0 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.module.modules.visuals;
import com.mojang.blaze3d.vertex.PoseStack;
import dev.loki.lovisual.config.values.*;
import dev.loki.lovisual.features.module.*;
import dev.loki.lovisual.features.module.Module;
import dev.loki.lovisual.util.block.*;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.renderer.SubmitNodeCollector;
import net.minecraft.core.BlockPos;
import net.minecraft.core.SectionPos;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
import dev.loki.lovisual.config.common.CommonSettingSchemas;
import dev.loki.lovisual.config.values.*;
import dev.loki.lovisual.features.gui.clickgui.settings.TextListSetting;
import dev.loki.lovisual.features.module.*;
import dev.loki.lovisual.features.module.modules.misc.DefineTarget;
import dev.loki.lovisual.render.engine.RenderState;
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
import dev.loki.lovisual.util.block.*;
import dev.loki.lovisual.util.block.bed.BedBlockUtil;
import dev.loki.lovisual.util.block.bed.BedwarsTeamColorUtil;
import dev.loki.lovisual.util.block.bed.SelfBedMode;
import dev.loki.lovisual.util.block.bed.SelfBedTracker;
import dev.loki.lovisual.util.logging.DebugLog;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
//todo Description
@ModuleInfo(
id = "bedwarsesp",
displayName = "BedwarsESP",
category = ModuleCategory.VISUALS
)
public class BedwarsESP extends Module {
//todo не доделан
private static final int SECTION_SCAN_BUDGET_PER_TICK = 4;
private static final int SECTION_SCAN_REFRESH_TICKS = 40;
private final Minecraft mc = Minecraft.getInstance();
private final WorldSectionBlockScanner sectionScanner = new WorldSectionBlockScanner();
private final EnumValue<BlockScanMode> modeValue =
enumCommon(
"bedwars_esp_mode",
"mode",
CommonSettingSchemas.ESP_SCAN_MODE,
BlockScanMode.LOS,
BlockScanMode.LOS,
BlockScanMode.UNRESTRICTED
);
private final BooleanValue limitDistanceValue =
boolCommon(
"bedwars_esp_limit_distance",
"limit_distance",
CommonSettingSchemas.ESP_LIMIT_DISTANCE,
true
);
private final NumberValue<Integer> maxDistanceValue =
visibleWhen(numCommon(
"bedwars_esp_max_distance",
"max_distance",
CommonSettingSchemas.ESP_MAX_DISTANCE,
64,
1,
256
), limitDistanceValue::get);
private final NumberValue<Integer> maxTargetsValue =
numCommon(
"bedwars_esp_max_targets",
"max_targets",
CommonSettingSchemas.ESP_MAX_TARGETS,
32,
1,
256
);
private final BlockScanContext scanContext = new BlockScanContext(mc, null);
private final BooleanValue useTracersValue =
boolCommon(
"bedwars_esp_use_tracers",
"use_tracers",
CommonSettingSchemas.ESP_USE_TRACERS,
false
);
private final NumberValue<Float> lineWidth =
numCommon(
"bedwars_esp_line_width",
"line_width",
CommonSettingSchemas.ESP_LINE_WIDTH,
1.5f,
0.5f,
4.0f
);
private final EnumValue<BlockOutlineShapeMode> outlineShapeMode =
enumCommon(
"bedwars_esp_outline_shape",
"outline_shape",
CommonSettingSchemas.ESP_OUTLINE_SHAPE,
BlockOutlineShapeMode.FULL_BLOCK,
BlockOutlineShapeMode.values()
);
private final EnumValue<SelfBedMode> selfBedMode =
enumMode(
"bedwars_esp_self_bed",
SelfBedMode.NONE,
SelfBedMode.values()
);
private final NumberValue<Float> spawnBedDistance =
visibleWhen(num(
"bedwars_esp_spawn_bed_distance",
"spawn_bed_distance",
24.0f,
16.0f,
48.0f
), () -> selfBedMode.get() == SelfBedMode.SPAWN_LOCATION);
private final NumberValue<Integer> maxLayers =
num(
"bedwars_esp_max_layers",
"max_layers",
1,
1,
5
);
private final BooleanValue showOpeningsValue =
bool(
"bedwars_esp_show_shell",
"show_openings",
true
);
private final RGBAColorValue selfBedColor =
color(
"bedwars_esp_self_bed_color",
"self_bed_color",
"#8026FF71"
);
private final RGBAColorValue otherBedColor =
color(
"bedwars_esp_other_bed_color",
"other_bed_color",
"#80FF6262"
);
private final RGBAColorValue selfOpeningColor =
visibleWhen(color(
"bedwars_esp_self_shell_color",
"self_opening_color",
"#4026BFFF"
), showOpeningsValue::get);
private final RGBAColorValue otherOpeningColor =
visibleWhen(color(
"bedwars_esp_other_shell_color",
"other_opening_color",
"#40FFC247"
), showOpeningsValue::get);
private final Map<BlockPos, ObservedBed> observedBedStates = new ConcurrentHashMap<>();
private final Map<BlockPos, BlockPos> bedPartToKey = new ConcurrentHashMap<>();
private final Set<Long> loggedBeds = ConcurrentHashMap.newKeySet();
private volatile Map<BlockPos, BedRenderTarget> renderTargets = new ConcurrentHashMap<>();
private ClientLevel lastWorld;
private BlockScanMode lastMode;
private int lastScanSignature = Integer.MIN_VALUE;
private int sectionScanRefreshTicks;
private static AABB createFaceBox(BlockPos bedPos, net.minecraft.core.Direction face) {
final double inset = 0.18;
final double depth = 0.045;
final double eps = 0.0025;
double minX = bedPos.getX();
double minY = bedPos.getY();
double minZ = bedPos.getZ();
double maxX = minX + 1.0;
double maxY = minY + 1.0;
double maxZ = minZ + 1.0;
return switch (face) {
case DOWN -> new AABB(minX + inset, minY - eps, minZ + inset, maxX - inset, minY + depth, maxZ - inset);
case UP -> new AABB(minX + inset, maxY - depth, minZ + inset, maxX - inset, maxY + eps, maxZ - inset);
case NORTH -> new AABB(minX + inset, minY + inset, minZ - eps, maxX - inset, maxY - inset, minZ + depth);
case SOUTH -> new AABB(minX + inset, minY + inset, maxZ - depth, maxX - inset, maxY - inset, maxZ + eps);
case WEST -> new AABB(minX - eps, minY + inset, minZ + inset, minX + depth, maxY - inset, maxZ - inset);
case EAST -> new AABB(maxX - depth, minY + inset, minZ + inset, maxX + eps, maxY - inset, maxZ - inset);
};
}
private static int multiplyAlpha(int argb, float factor) {
int alpha = (argb >>> 24) & 0xFF;
int nextAlpha = Math.max(0, Math.min(255, Math.round(alpha * factor)));
return (argb & 0x00FFFFFF) | (nextAlpha << 24);
}
private static int emphasizeAlpha(int argb) {
int alpha = (argb >>> 24) & 0xFF;
int nextAlpha = Math.max(alpha, 170);
return (argb & 0x00FFFFFF) | (Math.min(255, nextAlpha) << 24);
}
private static void addFilledBox(Renderer3D renderer, AABB box, int argb) {
int a = (argb >>> 24) & 0xFF;
int r = (argb >>> 16) & 0xFF;
int g = (argb >>> 8) & 0xFF;
int b = argb & 0xFF;
double minX = box.minX;
double minY = box.minY;
double minZ = box.minZ;
double maxX = box.maxX;
double maxY = box.maxY;
double maxZ = box.maxZ;
renderer.quad(minX, minY, minZ, maxX, minY, minZ, maxX, minY, maxZ, minX, minY, maxZ, r, g, b, a);
renderer.quad(minX, maxY, minZ, minX, maxY, maxZ, maxX, maxY, maxZ, maxX, maxY, minZ, r, g, b, a);
renderer.quad(minX, minY, maxZ, maxX, minY, maxZ, maxX, maxY, maxZ, minX, maxY, maxZ, r, g, b, a);
renderer.quad(minX, minY, minZ, minX, maxY, minZ, maxX, maxY, minZ, maxX, minY, minZ, r, g, b, a);
renderer.quad(maxX, minY, minZ, maxX, maxY, minZ, maxX, maxY, maxZ, maxX, minY, maxZ, r, g, b, a);
renderer.quad(minX, minY, minZ, minX, minY, maxZ, minX, maxY, maxZ, minX, maxY, minZ, r, g, b, a);
}
private static void addOutlineBox(Renderer3D renderer, AABB box, int argb) {
int a = (argb >>> 24) & 0xFF;
int r = (argb >>> 16) & 0xFF;
int g = (argb >>> 8) & 0xFF;
int b = argb & 0xFF;
double minX = box.minX;
double minY = box.minY;
double minZ = box.minZ;
double maxX = box.maxX;
double maxY = box.maxY;
double maxZ = box.maxZ;
renderer.line(minX, minY, minZ, maxX, minY, minZ, r, g, b, a);
renderer.line(maxX, minY, minZ, maxX, minY, maxZ, r, g, b, a);
renderer.line(maxX, minY, maxZ, minX, minY, maxZ, r, g, b, a);
renderer.line(minX, minY, maxZ, minX, minY, minZ, r, g, b, a);
renderer.line(minX, maxY, minZ, maxX, maxY, minZ, r, g, b, a);
renderer.line(maxX, maxY, minZ, maxX, maxY, maxZ, r, g, b, a);
renderer.line(maxX, maxY, maxZ, minX, maxY, maxZ, r, g, b, a);
renderer.line(minX, maxY, maxZ, minX, maxY, minZ, r, g, b, a);
renderer.line(minX, minY, minZ, minX, maxY, minZ, r, g, b, a);
renderer.line(maxX, minY, minZ, maxX, maxY, minZ, r, g, b, a);
renderer.line(maxX, minY, maxZ, maxX, maxY, maxZ, r, g, b, a);
renderer.line(minX, minY, maxZ, minX, maxY, maxZ, r, g, b, a);
}
@Override
public void onEnable() {
resetRuntimeState();
scanContext.refresh();
BlockObservationHub.requestSodiumRebuild(mc, scanBlockRadius());
requestLocalSectionScan();
}
@Override
public void onTick() {
if (!isEnabled() || mc.level == null) {
return;
}
if (lastWorld != mc.level) {
if (lastWorld != null) {
resetRuntimeState();
RenderSectionBlockScanner.clear();
}
lastWorld = mc.level;
}
scanContext.refresh();
if (mc.player == null) {
return;
}
BlockScanMode mode = modeValue.get();
if (lastMode != mode) {
clearRenderedBeds();
lastMode = mode;
requestLocalSectionScan();
}
maintainSectionScanQueue();
drainSectionScanQueue();
reconcileObservedBeds("cached");
}
@Override
public void onDisable() {
resetRuntimeState();
}
private void resetRuntimeState() {
clearTargets();
lastMode = null;
lastWorld = mc.level;
lastScanSignature = Integer.MIN_VALUE;
sectionScanRefreshTicks = 0;
sectionScanner.reset();
}
private void clearTargets() {
clearRenderedBeds();
observedBedStates.clear();
}
private void clearRenderedBeds() {
renderTargets = new ConcurrentHashMap<>();
bedPartToKey.clear();
loggedBeds.clear();
}
@Override
public WorldPhase getWorldPhase() {
return WorldPhase.END_MAIN;
}
@Override
public void onRenderWorld(PoseStack matrices, SubmitNodeCollector consumers) {
}
@Override
public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) {
if (!isEnabled() || mc.level == null || mc.player == null) return;
float previousLineWidth = RenderState.lineWidth;
RenderState.lineWidth = Math.max(0.5f, lineWidth.get());
try {
DefineTarget defineTarget = Modules.get(DefineTarget.class);
boolean useTeamColors = defineTarget != null && defineTarget.useBedwarsTeamColors();
for (BedRenderTarget target : renderTargets.values()) {
int bedColor = target.self() ? selfBedColor.getArgb() : otherBedColor.getArgb();
if (useTeamColors && target.teamColorRgb() != -1) {
bedColor = BedwarsTeamColorUtil.replaceRgb(bedColor, target.teamColorRgb());
}
for (BlockPos bedPart : target.bedParts()) {
BlockState state = mc.level.getBlockState(bedPart);
if (!BedBlockUtil.isBed(state)) {
continue;
}
BlockOutlineRenderer.render(renderer, mc.level, bedPart, state, outlineShapeMode.get(), emphasizeAlpha(bedColor));
}
if (!showOpeningsValue.get()) {
continue;
}
int openingColor = target.self() ? selfOpeningColor.getArgb() : otherOpeningColor.getArgb();
if (useTeamColors && target.teamColorRgb() != -1) {
openingColor = BedwarsTeamColorUtil.replaceRgb(openingColor, target.teamColorRgb());
}
for (FaceMarker opening : target.openingMarkers()) {
addFilledBox(renderer, opening.box(), multiplyAlpha(openingColor, 0.85f));
addOutlineBox(renderer, opening.box(), emphasizeAlpha(openingColor));
}
}
} finally {
RenderState.lineWidth = previousLineWidth;
}
renderTracers(renderer, tickDelta);
}
public void acceptSodiumBlockState(int x, int y, int z, BlockState state) {
if (!isEnabled() || !BedBlockUtil.isBed(state)) {
return;
}
recordObservedBlock(new BlockPos(x, y, z), state, false, RenderStatus.HIDDEN);
}
public void acceptSodiumRenderedBlockState(BlockPos pos, BlockState state) {
if (!isEnabled() || !BedBlockUtil.isBed(state)) {
return;
}
recordObservedBlock(pos, state, false, RenderStatus.VISIBLE);
}
public void acceptWorldBlockUpdate(BlockPos pos, BlockState state) {
if (!isEnabled()) {
return;
}
recordObservedBlock(pos, state, true);
}
private void recordObservedBlock(BlockPos rawPos, BlockState state) {
recordObservedBlock(rawPos, state, false, RenderStatus.PRESERVE);
}
private void recordObservedBlock(BlockPos rawPos, BlockState state, boolean forgetScannedSection) {
recordObservedBlock(rawPos, state, forgetScannedSection, RenderStatus.PRESERVE);
}
private void recordObservedBlock(BlockPos rawPos,
BlockState state,
boolean forgetScannedSection,
RenderStatus renderStatus) {
if (rawPos == null || state == null) {
return;
}
BlockPos pos = rawPos.immutable();
if (forgetScannedSection) {
sectionScanner.forget(pos);
}
if (!BedBlockUtil.isBed(state)) {
observedBedStates.remove(pos);
removeBedByPart(pos);
return;
}
RenderSectionBlockScanner.recordBlock(pos, state);
observedBedStates.compute(pos, (ignored, previous) -> {
boolean rendered = switch (renderStatus) {
case PRESERVE -> previous != null && previous.rendered();
case HIDDEN -> false;
case VISIBLE -> true;
};
return new ObservedBed(state, rendered);
});
}
private void updateObservedBed(BlockPos pos, BlockState state, String source) {
if (mc.level == null || mc.player == null || !withinDistance(pos)) {
removeBedByPart(pos);
return;
}
ObservedBed observed = observedBedStates.get(pos);
boolean visible = observed != null && observed.rendered();
if (!acceptsMode(pos, visible)) {
removeBedByPart(pos);
return;
}
BlockPos key = BedBlockUtil.canonicalBedPos(pos, state);
if (key == null) {
removeBedByPart(pos);
return;
}
key = key.immutable();
if (!renderTargets.containsKey(key) && renderTargets.size() >= maxTargetsValue.get()) {
return;
}
Map<BlockPos, BedRenderTarget> built = buildBedTargets(List.of(
new RenderSectionBlockScanner.ScanResult(pos, state, visible)
));
BedRenderTarget target = built.get(key);
if (target == null) {
return;
}
BedRenderTarget previous = renderTargets.put(key, target);
if (previous != null) {
for (BlockPos bedPart : previous.bedParts()) {
bedPartToKey.remove(bedPart);
}
}
for (BlockPos bedPart : target.bedParts()) {
bedPartToKey.put(bedPart, key);
}
if (loggedBeds.add(key.asLong())) {
DebugLog.info(
"[BedwarsESP] detected %s at %s visible=%s source=%s mode=%s",
BuiltInRegistries.BLOCK.getKey(state.getBlock()),
key.toShortString(),
visible,
source,
modeValue.get().getId()
);
}
}
private void reconcileObservedBeds(String source) {
for (Map.Entry<BlockPos, ObservedBed> entry : observedBedStates.entrySet()) {
BlockPos pos = entry.getKey();
ObservedBed observed = entry.getValue();
BlockState state = observed.state();
if (mc.level != null && withinDistance(pos)) {
BlockState currentState = mc.level.getBlockState(pos);
if (BedBlockUtil.isBed(currentState)) {
if (currentState != state) {
observedBedStates.put(pos, new ObservedBed(currentState, observed.rendered()));
state = currentState;
}
} else {
observedBedStates.remove(pos);
removeBedByPart(pos);
continue;
}
}
updateObservedBed(pos, state, source);
}
}
private void removeBedByPart(BlockPos pos) {
BlockPos key = bedPartToKey.remove(pos);
if (key == null) {
return;
}
BedRenderTarget removed = renderTargets.remove(key);
loggedBeds.remove(key.asLong());
if (removed != null) {
for (BlockPos bedPart : removed.bedParts()) {
bedPartToKey.remove(bedPart);
}
}
}
private void maintainSectionScanQueue() {
if (mc.level == null || mc.player == null) {
return;
}
int signature = currentScanSignature();
if (signature != lastScanSignature || ++sectionScanRefreshTicks >= SECTION_SCAN_REFRESH_TICKS) {
lastScanSignature = signature;
sectionScanRefreshTicks = 0;
requestLocalSectionScan();
}
}
private void requestLocalSectionScan() {
if (mc.level == null || mc.player == null) {
return;
}
int radius = scanBlockRadius();
int minY;
int maxY;
if (limitDistanceValue.get()) {
int eyeY = (int) Math.floor(mc.player.getEyePosition().y);
minY = eyeY - radius;
maxY = eyeY + radius;
} else {
minY = mc.level.getMinY();
maxY = mc.level.getMaxY();
}
sectionScanner.enqueueAround(mc, radius, minY, maxY);
}
private void drainSectionScanQueue() {
sectionScanner.drain(
mc,
SECTION_SCAN_BUDGET_PER_TICK,
BedBlockUtil::isBed,
this::recordObservedBlock
);
}
private int scanBlockRadius() {
return limitDistanceValue.get() ? Math.max(1, maxDistanceValue.get()) : 256;
}
private int currentScanSignature() {
int centerChunkX = SectionPos.blockToSectionCoord(mc.player.getBlockX());
int centerChunkZ = SectionPos.blockToSectionCoord(mc.player.getBlockZ());
int centerSectionY = SectionPos.blockToSectionCoord(mc.player.getBlockY());
int result = 17;
result = 31 * result + centerChunkX;
result = 31 * result + centerChunkZ;
result = 31 * result + centerSectionY;
result = 31 * result + (limitDistanceValue.get() ? 1 : 0);
result = 31 * result + maxDistanceValue.get();
result = 31 * result + maxLayers.get();
return result;
}
private boolean withinDistance(BlockPos pos) {
if (!limitDistanceValue.get() || mc.player == null) {
return true;
}
Vec3 eye = mc.player.getEyePosition();
double dx = pos.getX() + 0.5 - eye.x;
double dy = pos.getY() + 0.5 - eye.y;
double dz = pos.getZ() + 0.5 - eye.z;
int maxDistance = maxDistanceValue.get();
return dx * dx + dy * dy + dz * dz <= (double) maxDistance * (double) maxDistance;
}
private boolean acceptsMode(BlockPos pos, boolean visible) {
return switch (modeValue.get()) {
case LOS -> visible;
case UNRESTRICTED -> true;
};
}
private Map<BlockPos, BedRenderTarget> buildBedTargets(Collection<RenderSectionBlockScanner.ScanResult> scanResults) {
Map<BlockPos, MutableBedTarget> grouped = new HashMap<>();
for (RenderSectionBlockScanner.ScanResult result : scanResults) {
BlockState state = result.state();
BlockPos key = BedBlockUtil.canonicalBedPos(result.pos(), state);
if (key == null) {
continue;
}
MutableBedTarget target = grouped.computeIfAbsent(key, ignored -> new MutableBedTarget());
target.sourcePos = result.pos();
target.sourceState = state;
target.visible |= result.visible();
Integer teamColorRgb = BedwarsTeamColorUtil.getBedTeamRgb(state);
if (teamColorRgb != null) {
target.teamColorRgb = teamColorRgb;
}
target.self |= SelfBedTracker.INSTANCE.isSelfBed(
selfBedMode.get(),
state,
result.pos(),
mc.player,
spawnBedDistance.get()
);
target.bedParts.add(result.pos().immutable());
net.minecraft.core.Direction direction = BedBlockUtil.anotherBedPartDirection(state);
if (direction != null) {
target.bedParts.add(result.pos().relative(direction).immutable());
}
}
Map<BlockPos, BedRenderTarget> next = new HashMap<>(grouped.size());
for (Map.Entry<BlockPos, MutableBedTarget> entry : grouped.entrySet()) {
MutableBedTarget value = entry.getValue();
if (value.sourcePos == null || value.sourceState == null || value.bedParts.isEmpty()) {
continue;
}
List<AABB> bedBoxes = new ArrayList<>(value.bedParts.size());
double cx = 0.0;
double cy = 0.0;
double cz = 0.0;
for (BlockPos bedPart : value.bedParts) {
bedBoxes.add(new AABB(bedPart));
Vec3 center = Vec3.atCenterOf(bedPart);
cx += center.x;
cy += center.y;
cz += center.z;
}
Vec3 tracerCenter = new Vec3(cx / value.bedParts.size(), cy / value.bedParts.size(), cz / value.bedParts.size());
List<FaceMarker> openingMarkers = List.of();
if (showOpeningsValue.get()) {
LinkedHashSet<BlockPos> shellPositions = new LinkedHashSet<>();
List<BedBlockUtil.LayeredBlockPos> layerPositions = BedBlockUtil.searchBedLayer(
value.sourcePos,
value.sourceState,
maxLayers.get()
);
layerPositions.sort(Comparator.comparingInt(BedBlockUtil.LayeredBlockPos::layer));
for (BedBlockUtil.LayeredBlockPos layerPos : layerPositions) {
shellPositions.add(layerPos.pos());
}
openingMarkers = collectOpeningMarkers(value.bedParts, shellPositions);
}
next.put(entry.getKey(), new BedRenderTarget(
List.copyOf(value.bedParts),
List.copyOf(bedBoxes),
openingMarkers,
tracerCenter,
value.self,
value.visible,
value.teamColorRgb
));
}
return next;
}
private List<FaceMarker> collectOpeningMarkers(Collection<BlockPos> bedParts, Collection<BlockPos> shellPositions) {
if (bedParts.isEmpty()) {
return List.of();
}
Set<BlockPos> bedSet = new HashSet<>(bedParts);
int minX = Integer.MAX_VALUE;
int minY = Integer.MAX_VALUE;
int minZ = Integer.MAX_VALUE;
int maxX = Integer.MIN_VALUE;
int maxY = Integer.MIN_VALUE;
int maxZ = Integer.MIN_VALUE;
for (BlockPos pos : bedParts) {
minX = Math.min(minX, pos.getX());
minY = Math.min(minY, pos.getY());
minZ = Math.min(minZ, pos.getZ());
maxX = Math.max(maxX, pos.getX());
maxY = Math.max(maxY, pos.getY());
maxZ = Math.max(maxZ, pos.getZ());
}
for (BlockPos pos : shellPositions) {
minX = Math.min(minX, pos.getX());
minY = Math.min(minY, pos.getY());
minZ = Math.min(minZ, pos.getZ());
maxX = Math.max(maxX, pos.getX());
maxY = Math.max(maxY, pos.getY());
maxZ = Math.max(maxZ, pos.getZ());
}
minX--;
minY--;
minZ--;
maxX++;
maxY++;
maxZ++;
ArrayDeque<BlockPos> queue = new ArrayDeque<>();
Set<BlockPos> reachable = new HashSet<>();
BlockPos.MutableBlockPos mutable = new BlockPos.MutableBlockPos();
for (int x = minX; x <= maxX; x++) {
for (int y = minY; y <= maxY; y++) {
seedOpeningBoundary(queue, reachable, bedSet, mutable.set(x, y, minZ));
seedOpeningBoundary(queue, reachable, bedSet, mutable.set(x, y, maxZ));
}
}
for (int x = minX; x <= maxX; x++) {
for (int z = minZ; z <= maxZ; z++) {
seedOpeningBoundary(queue, reachable, bedSet, mutable.set(x, minY, z));
seedOpeningBoundary(queue, reachable, bedSet, mutable.set(x, maxY, z));
}
}
for (int y = minY; y <= maxY; y++) {
for (int z = minZ; z <= maxZ; z++) {
seedOpeningBoundary(queue, reachable, bedSet, mutable.set(minX, y, z));
seedOpeningBoundary(queue, reachable, bedSet, mutable.set(maxX, y, z));
}
}
while (!queue.isEmpty()) {
BlockPos current = queue.removeFirst();
for (net.minecraft.core.Direction direction : BlockScanContext.DIRECTIONS) {
mutable.set(current).move(direction);
if (mutable.getX() < minX || mutable.getX() > maxX
|| mutable.getY() < minY || mutable.getY() > maxY
|| mutable.getZ() < minZ || mutable.getZ() > maxZ) {
continue;
}
seedOpeningBoundary(queue, reachable, bedSet, mutable);
}
}
LinkedHashSet<FaceKey> openings = new LinkedHashSet<>();
for (BlockPos airPos : reachable) {
for (net.minecraft.core.Direction direction : BlockScanContext.DIRECTIONS) {
BlockPos neighbor = airPos.relative(direction);
if (!bedSet.contains(neighbor)) {
continue;
}
openings.add(new FaceKey(neighbor.immutable(), direction.getOpposite()));
}
}
if (openings.isEmpty()) {
return List.of();
}
List<FaceMarker> result = new ArrayList<>(openings.size());
for (FaceKey opening : openings) {
result.add(new FaceMarker(createFaceBox(opening.pos(), opening.face())));
}
return List.copyOf(result);
}
private void seedOpeningBoundary(ArrayDeque<BlockPos> queue,
Set<BlockPos> reachable,
Set<BlockPos> bedSet,
BlockPos pos) {
if (bedSet.contains(pos) || !scanContext.isPassable(pos)) {
return;
}
BlockPos immutable = pos.immutable();
if (reachable.add(immutable)) {
queue.addLast(immutable);
}
}
private void renderTracers(Renderer3D renderer, float tickDelta) {
Tracers tracers = Modules.get(Tracers.class);
if (!useTracersValue.get() || tracers == null) return;
DefineTarget defineTarget = Modules.get(DefineTarget.class);
boolean useTeamColors = defineTarget != null && defineTarget.useBedwarsTeamColors();
for (BedRenderTarget target : renderTargets.values()) {
int color = target.self() ? selfBedColor.getArgb() : otherBedColor.getArgb();
if (useTeamColors && target.teamColorRgb() != -1) {
color = BedwarsTeamColorUtil.replaceRgb(color, target.teamColorRgb());
}
tracers.render3DTracersRaw(renderer, tickDelta, List.of(target.tracerCenter()), color);
}
}
private static final class MutableBedTarget {
private final LinkedHashSet<BlockPos> bedParts = new LinkedHashSet<>();
private BlockPos sourcePos;
private BlockState sourceState;
private boolean self;
private boolean visible;
private int teamColorRgb = -1;
}
private enum RenderStatus {
PRESERVE,
HIDDEN,
VISIBLE
}
private record ObservedBed(BlockState state, boolean rendered) {
}
private record FaceKey(BlockPos pos, net.minecraft.core.Direction face) {
}
private record FaceMarker(AABB box) {
}
private record BedRenderTarget(
List<BlockPos> bedParts,
List<AABB> bedBoxes,
List<FaceMarker> openingMarkers,
Vec3 tracerCenter,
boolean self,
boolean visible,
int teamColorRgb
) {
}
}

View file

@ -1,835 +0,0 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.module.modules.visuals;
import com.mojang.blaze3d.vertex.PoseStack;
import dev.loki.lovisual.config.values.*;
import dev.loki.lovisual.features.module.*;
import dev.loki.lovisual.features.module.Module;
import dev.loki.lovisual.util.block.*;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.renderer.SubmitNodeCollector;
import net.minecraft.core.BlockPos;
import net.minecraft.core.SectionPos;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
import dev.loki.lovisual.config.common.CommonSettingSchemas;
import dev.loki.lovisual.config.values.*;
import dev.loki.lovisual.features.gui.clickgui.settings.TextListSetting;
import dev.loki.lovisual.features.module.*;
import dev.loki.lovisual.render.engine.RenderState;
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
import dev.loki.lovisual.util.block.*;
import dev.loki.lovisual.util.logging.DebugLog;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
//todo Description
@ModuleInfo(
id = "blockesp",
displayName = "BlockESP",
aliases = {"xray", "oreesp", "blocks"},
category = ModuleCategory.VISUALS
)
public class BlockESP extends Module {
private static final int SECTION_SCAN_BUDGET_PER_TICK = 10;
private static final int SECTION_SCAN_BURST_BUDGET_PER_TICK = 64;
private static final int SECTION_SCAN_BURST_TICKS = 4;
private static final int SECTION_SCAN_REFRESH_TICKS = 40;
private static final int SODIUM_CANDIDATE_DRAIN_BUDGET_PER_TICK = 4096;
private static final int RECONCILE_BUDGET_PER_TICK = 512;
private static final int FULL_RECONCILE_REFRESH_TICKS = 20;
private final Minecraft mc = Minecraft.getInstance();
private final BlockDeobfuscationScanner deobfuscationScanner = new BlockDeobfuscationScanner();
private final WorldSectionBlockScanner sectionScanner = new WorldSectionBlockScanner();
private final EnumValue<BlockScanMode> modeValue =
enumCommon(
"blockEspMode",
"mode",
CommonSettingSchemas.ESP_SCAN_MODE,
BlockScanMode.LOS,
BlockScanMode.values()
);
private final BooleanValue deobfuscationValue =
bool(
"blockEspDeobfuscation",
"deobfuscation",
false
);
private final BooleanValue limitDistanceValue =
boolCommon(
"blockEspLimitDistance",
"limit_distance",
CommonSettingSchemas.ESP_LIMIT_DISTANCE,
true
);
private final NumberValue<Integer> maxDistanceValue =
visibleWhen(numCommon(
"blockEspMaxDistance",
"max_distance",
CommonSettingSchemas.ESP_MAX_DISTANCE,
64,
1,
256
), limitDistanceValue::get);
private final NumberValue<Integer> maxTargetsValue =
numCommon(
"blockEspMaxTargets",
"max_targets",
CommonSettingSchemas.ESP_MAX_TARGETS,
128,
1,
4096
);
private final NumberValue<Integer> deobfuscationRadiusValue =
visibleWhen(num(
"blockEspDeobfuscationRadius",
"deobfuscation_radius",
5,
1,
64
), deobfuscationValue::get);
private final NumberValue<Integer> deobfuscationUpValue =
visibleWhen(num(
"blockEspDeobfuscationUp",
"deobfuscation_up",
5,
1,
32
), deobfuscationValue::get);
private final NumberValue<Integer> deobfuscationDownValue =
visibleWhen(num(
"blockEspDeobfuscationDown",
"deobfuscation_down",
5,
1,
32
), deobfuscationValue::get);
private final BooleanValue deobfuscationFastValue =
visibleWhen(bool(
"blockEspDeobfuscationFast",
"deobfuscation_fast",
false
), deobfuscationValue::get);
private final ItemIdSetValue searchBlocksValue =
itemList(
"blockEspSearchBlocks",
"search_blocks",
TextListSetting.PickerMode.BLOCKS
);
private final BlockScanContext scanContext = new BlockScanContext(mc, searchBlocksValue);
private final BooleanValue useTracersValue =
boolCommon(
"blockEspUseTracers",
"use_tracers",
CommonSettingSchemas.ESP_USE_TRACERS,
false
);
private final EnumValue<BlockOutlineShapeMode> outlineShapeMode =
enumCommon(
"blockEspOutlineShape",
"outline_shape",
CommonSettingSchemas.ESP_OUTLINE_SHAPE,
BlockOutlineShapeMode.FULL_BLOCK,
BlockOutlineShapeMode.values()
);
private final NumberValue<Float> lineWidth =
numCommon(
"blockEspLineWidth",
"line_width",
CommonSettingSchemas.ESP_LINE_WIDTH,
1.5f,
0.5f,
4.0f
);
private final RGBColorValue visibleColor =
colorNoAlpha(
"blockEspVisibleColor",
"visible_color",
"#FF0000"
);
private final RGBColorValue chunkColor =
colorNoAlpha(
"blockEspChunkColor",
"chunk_color",
"#FFFF00"
);
private final BooleanValue clusterTracersValue =
visibleWhen(bool(
"blockEspClusterTracers",
"cluster_tracers",
false
), useTracersValue::get);
private final Map<BlockPos, ObservedTarget> observedTargetStates = new ConcurrentHashMap<>();
private final Set<Long> loggedTargets = ConcurrentHashMap.newKeySet();
private final ArrayDeque<BlockPos> reconcileQueue = new ArrayDeque<>();
private final Set<Long> queuedReconcileTargets = ConcurrentHashMap.newKeySet();
private volatile Map<BlockPos, Target> renderTargets = new ConcurrentHashMap<>();
private ClientLevel lastWorld;
private BlockScanMode lastMode;
private int lastSearchBlocksHash;
private int lastScanSignature = Integer.MIN_VALUE;
private int sectionScanRefreshTicks;
private int sectionScanBurstTicks;
private int fastSnapshotGeneration;
private int fullReconcileRefreshTicks;
@Override
public void onEnable() {
resetRuntimeState();
scanContext.refresh();
lastSearchBlocksHash = currentSearchBlocksHash();
publishFastScanSnapshot();
BlockObservationHub.requestSodiumRebuild(mc, scanBlockRadius());
requestLocalSectionScan(true);
}
@Override
public void onDisable() {
BlockEspSodiumCandidateCollector.disable();
resetRuntimeState();
}
@Override
public void onTick() {
if (!isEnabled() || mc.level == null) {
BlockEspSodiumCandidateCollector.disable();
return;
}
if (lastWorld != mc.level) {
if (lastWorld != null) {
resetRuntimeState();
RenderSectionBlockScanner.clear();
}
lastWorld = mc.level;
}
scanContext.refresh();
if (mc.player == null) {
BlockEspSodiumCandidateCollector.disable();
return;
}
publishFastScanSnapshot();
int searchBlocksHash = currentSearchBlocksHash();
if (lastSearchBlocksHash != searchBlocksHash) {
clearObservedTargets();
deobfuscationScanner.reset();
sectionScanner.reset();
lastSearchBlocksHash = searchBlocksHash;
fastSnapshotGeneration++;
publishFastScanSnapshot();
BlockObservationHub.requestSodiumRebuild(mc, scanBlockRadius());
requestLocalSectionScan(true);
}
BlockScanMode mode = modeValue.get();
if (lastMode != mode) {
renderTargets = new ConcurrentHashMap<>();
loggedTargets.clear();
reconcileQueue.clear();
queuedReconcileTargets.clear();
lastMode = mode;
requestLocalSectionScan(true);
}
drainSodiumCandidateQueue();
maintainSectionScanQueue();
drainSectionScanQueue();
if (deobfuscationValue.get()) {
deobfuscationScanner.tick(
mc,
1,
deobfuscationRadiusValue.get(),
deobfuscationUpValue.get(),
deobfuscationDownValue.get(),
deobfuscationFastValue.get()
);
mergeDeobfuscatedTargets();
}
maintainReconcileQueue();
drainReconcileQueue("cached", RECONCILE_BUDGET_PER_TICK);
}
public void acceptSodiumBlockState(int x, int y, int z, BlockState state) {
if (!isEnabled() || !scanContext.isConfiguredTarget(state)) {
return;
}
acceptObservedCandidate(BlockPos.asLong(x, y, z), state, "sodium-direct", RenderStatus.HIDDEN);
}
public void acceptWorldBlockUpdate(BlockPos pos, BlockState state) {
if (!isEnabled()) {
return;
}
acceptBlockUpdate(pos, state);
}
private void acceptBlockUpdate(BlockPos pos, BlockState state) {
if (pos == null || state == null) {
return;
}
sectionScanner.forget(pos);
scanContext.refresh();
boolean target = scanContext.isConfiguredTarget(state);
boolean deobfuscatedPositive = deobfuscationScanner.hasPositiveState(pos);
deobfuscationScanner.acceptBlockUpdate(pos, state, scanContext::isConfiguredTarget);
if (!target && deobfuscatedPositive) {
BlockState positiveState = deobfuscationScanner.getPositiveState(pos);
if (scanContext.isConfiguredTarget(positiveState)) {
recordTargetCandidate(pos, positiveState);
updateObservedTarget(pos, positiveState, "deobfuscation-update");
}
return;
}
if (!target) {
removeTargetCandidate(pos);
return;
}
recordTargetCandidate(pos, state);
updateObservedTarget(pos, state, "world-update");
if (deobfuscationValue.get()) {
DebugLog.info("[BlockESP] deobfuscated %s at %s", BuiltInRegistries.BLOCK.getKey(state.getBlock()), pos.toShortString());
}
}
private void resetRuntimeState() {
clearObservedTargets();
lastMode = null;
lastWorld = mc.level;
lastSearchBlocksHash = currentSearchBlocksHash();
lastScanSignature = Integer.MIN_VALUE;
sectionScanRefreshTicks = 0;
sectionScanBurstTicks = 0;
fullReconcileRefreshTicks = 0;
fastSnapshotGeneration++;
BlockEspSodiumCandidateCollector.clear();
deobfuscationScanner.reset();
sectionScanner.reset();
}
private void clearObservedTargets() {
renderTargets = new ConcurrentHashMap<>();
observedTargetStates.clear();
loggedTargets.clear();
reconcileQueue.clear();
queuedReconcileTargets.clear();
BlockEspSodiumCandidateCollector.clear();
}
private void recordTargetCandidate(BlockPos rawPos, BlockState state) {
recordTargetCandidate(rawPos, state, RenderStatus.PRESERVE);
}
private void recordTargetCandidate(BlockPos rawPos, BlockState state, RenderStatus renderStatus) {
if (rawPos == null || state == null) {
return;
}
BlockPos pos = rawPos.immutable();
RenderSectionBlockScanner.recordBlock(pos, state);
observedTargetStates.compute(pos, (ignored, previous) -> {
boolean rendered = switch (renderStatus) {
case PRESERVE -> previous != null && previous.rendered();
case HIDDEN -> false;
case VISIBLE -> true;
};
return new ObservedTarget(state, rendered);
});
enqueueReconcile(pos);
}
private void removeTargetCandidate(BlockPos pos) {
if (pos == null) {
return;
}
observedTargetStates.remove(pos);
renderTargets.remove(pos);
loggedTargets.remove(pos.asLong());
queuedReconcileTargets.remove(pos.asLong());
sectionScanner.forget(pos);
if (mc.level != null) {
RenderSectionBlockScanner.recordBlock(pos, mc.level.getBlockState(pos));
}
}
private void updateObservedTarget(BlockPos pos, BlockState state, String source) {
if (mc.level == null || mc.player == null || !withinDistance(pos)) {
renderTargets.remove(pos);
return;
}
ObservedTarget observed = observedTargetStates.get(pos);
boolean visible = observed != null && observed.rendered();
if (!acceptsMode(pos, visible)) {
renderTargets.remove(pos);
return;
}
if (!renderTargets.containsKey(pos) && renderTargets.size() >= maxTargetsValue.get()) {
return;
}
Target target = new Target();
target.pos = pos;
target.state = state;
target.box = new AABB(pos);
target.visible = visible;
renderTargets.put(pos, target);
if (loggedTargets.add(pos.asLong())) {
DebugLog.info(
"[BlockESP] detected %s at %s visible=%s source=%s mode=%s",
BuiltInRegistries.BLOCK.getKey(state.getBlock()),
pos.toShortString(),
visible,
source,
modeValue.get().getId()
);
}
}
private void drainSodiumCandidateQueue() {
BlockEspSodiumCandidateCollector.drain(
SODIUM_CANDIDATE_DRAIN_BUDGET_PER_TICK,
candidate -> {
if (candidate.generation() == fastSnapshotGeneration) {
RenderStatus renderStatus = candidate.source() == BlockEspSodiumCandidateCollector.CandidateSource.SODIUM_BUFFERED_QUAD
? RenderStatus.VISIBLE
: RenderStatus.HIDDEN;
acceptObservedCandidate(
candidate.packedPos(),
candidate.state(),
candidate.source().name().toLowerCase(),
renderStatus
);
}
}
);
}
private void acceptObservedCandidate(long packedPos, BlockState hintedState, String source) {
acceptObservedCandidate(packedPos, hintedState, source, RenderStatus.PRESERVE);
}
private void acceptObservedCandidate(long packedPos, BlockState hintedState, String source, RenderStatus renderStatus) {
if (mc.level == null || mc.player == null) {
return;
}
BlockPos pos = BlockPos.of(packedPos);
if (!withinDistance(pos)) {
removeTargetCandidate(pos);
return;
}
BlockState currentState = mc.level.getBlockState(pos);
BlockState state = scanContext.isConfiguredTarget(currentState) ? currentState : hintedState;
if (!scanContext.isConfiguredTarget(state)) {
removeTargetCandidate(pos);
return;
}
recordTargetCandidate(pos, state, renderStatus);
updateObservedTarget(pos, state, source);
}
private void enqueueReconcile(BlockPos pos) {
if (pos == null) {
return;
}
long key = pos.asLong();
if (queuedReconcileTargets.add(key)) {
reconcileQueue.addLast(pos.immutable());
}
}
private void maintainReconcileQueue() {
if (++fullReconcileRefreshTicks < FULL_RECONCILE_REFRESH_TICKS) {
return;
}
fullReconcileRefreshTicks = 0;
for (BlockPos pos : observedTargetStates.keySet()) {
enqueueReconcile(pos);
}
}
private void drainReconcileQueue(String source, int maxEntries) {
int processed = 0;
while (processed < maxEntries && !reconcileQueue.isEmpty()) {
BlockPos pos = reconcileQueue.removeFirst();
queuedReconcileTargets.remove(pos.asLong());
ObservedTarget observed = observedTargetStates.get(pos);
if (observed == null) {
processed++;
continue;
}
BlockState state = observed.state();
if (mc.level != null && withinDistance(pos)) {
BlockState currentState = mc.level.getBlockState(pos);
if (scanContext.isConfiguredTarget(currentState)) {
if (currentState != state) {
observedTargetStates.put(pos, new ObservedTarget(currentState, observed.rendered()));
state = currentState;
}
} else if (!deobfuscationScanner.hasPositiveState(pos)) {
removeTargetCandidate(pos);
processed++;
continue;
}
}
updateObservedTarget(pos, state, source);
processed++;
}
}
private void maintainSectionScanQueue() {
if (mc.level == null || mc.player == null) {
return;
}
int signature = currentScanSignature();
if (signature != lastScanSignature || ++sectionScanRefreshTicks >= SECTION_SCAN_REFRESH_TICKS) {
lastScanSignature = signature;
sectionScanRefreshTicks = 0;
requestLocalSectionScan(false);
}
}
private void requestLocalSectionScan(boolean burst) {
if (mc.level == null || mc.player == null) {
return;
}
int radius = scanBlockRadius();
int minY;
int maxY;
if (limitDistanceValue.get()) {
int eyeY = (int) Math.floor(mc.player.getEyePosition().y);
minY = eyeY - radius;
maxY = eyeY + radius;
} else {
minY = mc.level.getMinY();
maxY = mc.level.getMaxY();
}
sectionScanner.enqueueAround(mc, radius, minY, maxY);
if (burst) {
sectionScanBurstTicks = SECTION_SCAN_BURST_TICKS;
}
}
private void drainSectionScanQueue() {
int budget = sectionScanBurstTicks > 0 ? SECTION_SCAN_BURST_BUDGET_PER_TICK : SECTION_SCAN_BUDGET_PER_TICK;
if (sectionScanBurstTicks > 0) {
sectionScanBurstTicks--;
}
sectionScanner.drain(
mc,
budget,
scanContext::isConfiguredTarget,
(pos, state) -> acceptObservedCandidate(pos.asLong(), state, "section")
);
}
private void publishFastScanSnapshot() {
if (!isEnabled() || mc.player == null) {
BlockEspSodiumCandidateCollector.disable();
return;
}
BlockEspSodiumCandidateCollector.publishSnapshot(
true,
scanContext.targetBlocksSnapshot(),
limitDistanceValue.get(),
scanBlockRadius(),
mc.player.getBlockX(),
mc.player.getBlockY(),
mc.player.getBlockZ(),
fastSnapshotGeneration
);
}
private int scanBlockRadius() {
return limitDistanceValue.get() ? Math.max(1, maxDistanceValue.get()) : 256;
}
private int currentSearchBlocksHash() {
Set<String> targets = searchBlocksValue.get();
return targets == null ? 0 : targets.hashCode();
}
private int currentScanSignature() {
int centerChunkX = SectionPos.blockToSectionCoord(mc.player.getBlockX());
int centerChunkZ = SectionPos.blockToSectionCoord(mc.player.getBlockZ());
int centerSectionY = SectionPos.blockToSectionCoord(mc.player.getBlockY());
int result = 17;
result = 31 * result + centerChunkX;
result = 31 * result + centerChunkZ;
result = 31 * result + centerSectionY;
result = 31 * result + (limitDistanceValue.get() ? 1 : 0);
result = 31 * result + maxDistanceValue.get();
result = 31 * result + currentSearchBlocksHash();
return result;
}
private void mergeDeobfuscatedTargets() {
for (RenderSectionBlockScanner.ScanResult result : deobfuscationScanner.snapshot(
mc,
scanContext::isConfiguredTarget,
null,
maxTargetsValue.get()
)) {
acceptObservedCandidate(result.pos().asLong(), result.state(), "deobfuscation");
}
}
private boolean withinDistance(BlockPos pos) {
if (!limitDistanceValue.get() || mc.player == null) {
return true;
}
Vec3 eye = mc.player.getEyePosition();
double dx = pos.getX() + 0.5 - eye.x;
double dy = pos.getY() + 0.5 - eye.y;
double dz = pos.getZ() + 0.5 - eye.z;
int maxDistance = maxDistanceValue.get();
return dx * dx + dy * dy + dz * dz <= (double) maxDistance * (double) maxDistance;
}
private boolean isCurrentTargetBlock(BlockPos pos) {
if (mc.level == null || pos == null) {
return false;
}
return scanContext.isConfiguredTarget(mc.level.getBlockState(pos));
}
private boolean acceptsMode(BlockPos pos, boolean visible) {
return switch (modeValue.get()) {
case LOS -> visible;
case UNRESTRICTED -> true;
};
}
@Override
public WorldPhase getWorldPhase() {
return WorldPhase.END_MAIN;
}
@Override
public void onRenderWorld(PoseStack matrices, SubmitNodeCollector consumers) {
}
@Override
public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) {
if (!isEnabled() || mc.level == null || mc.player == null) {
return;
}
float previousLineWidth = RenderState.lineWidth;
RenderState.lineWidth = Math.max(0.5f, lineWidth.get());
try {
for (Target target : renderTargets.values()) {
if (!withinDistance(target.pos)) {
continue;
}
BlockState state = target.state;
if (mc.level != null) {
BlockState current = mc.level.getBlockState(target.pos);
if (!scanContext.isConfiguredTarget(current)) {
removeTargetCandidate(target.pos);
continue;
}
state = current;
if (!state.equals(target.state)) {
ObservedTarget observed = observedTargetStates.get(target.pos);
boolean rendered = observed != null && observed.rendered();
observedTargetStates.put(target.pos, new ObservedTarget(state, rendered));
target.state = state;
}
}
int color = 0xFF000000 | ((target.visible ? visibleColor.getArgb() : chunkColor.getArgb()) & 0x00FFFFFF);
BlockOutlineRenderer.render(renderer, mc.level, target.pos, state, outlineShapeMode.get(), color);
}
} finally {
RenderState.lineWidth = previousLineWidth;
}
renderBlockTracers(renderer, tickDelta);
}
private void drawBox(Renderer3D renderer, AABB box, int r, int g, int b, int a) {
double x1 = box.minX;
double y1 = box.minY;
double z1 = box.minZ;
double x2 = box.maxX;
double y2 = box.maxY;
double z2 = box.maxZ;
line(renderer, x1, y1, z1, x2, y1, z1, r, g, b, a);
line(renderer, x2, y1, z1, x2, y1, z2, r, g, b, a);
line(renderer, x2, y1, z2, x1, y1, z2, r, g, b, a);
line(renderer, x1, y1, z2, x1, y1, z1, r, g, b, a);
line(renderer, x1, y2, z1, x2, y2, z1, r, g, b, a);
line(renderer, x2, y2, z1, x2, y2, z2, r, g, b, a);
line(renderer, x2, y2, z2, x1, y2, z2, r, g, b, a);
line(renderer, x1, y2, z2, x1, y2, z1, r, g, b, a);
line(renderer, x1, y1, z1, x1, y2, z1, r, g, b, a);
line(renderer, x2, y1, z1, x2, y2, z1, r, g, b, a);
line(renderer, x2, y1, z2, x2, y2, z2, r, g, b, a);
line(renderer, x1, y1, z2, x1, y2, z2, r, g, b, a);
}
private void line(Renderer3D renderer,
double x1, double y1, double z1,
double x2, double y2, double z2,
int r, int g, int b, int a) {
renderer.line(x1, y1, z1, x2, y2, z2, r, g, b, a);
}
private void renderBlockTracers(Renderer3D renderer, float tickDelta) {
Tracers tracers = Modules.get(Tracers.class);
if (!useTracersValue.get() || tracers == null) {
return;
}
if (!clusterTracersValue.get()) {
for (Target target : renderTargets.values()) {
if (!withinDistance(target.pos) || !isCurrentTargetBlock(target.pos)) {
continue;
}
int color = target.visible ? visibleColor.getArgb() : chunkColor.getArgb();
tracers.render3DTracersRaw(renderer, tickDelta, List.of(Vec3.atCenterOf(target.pos)), color);
}
return;
}
List<List<Target>> clusters = buildClusters(renderTargets.values().stream()
.filter(target -> withinDistance(target.pos) && isCurrentTargetBlock(target.pos))
.toList());
for (List<Target> cluster : clusters) {
boolean anyVisible = cluster.stream().anyMatch(target -> target.visible);
int color = anyVisible ? visibleColor.getArgb() : chunkColor.getArgb();
tracers.render3DTracersRaw(renderer, tickDelta, List.of(clusterCenter(cluster)), color);
}
}
private List<List<Target>> buildClusters(Collection<Target> targets) {
Map<BlockPos, Target> map = new HashMap<>();
for (Target target : targets) {
map.put(target.pos, target);
}
Set<BlockPos> visited = new HashSet<>();
List<List<Target>> clusters = new ArrayList<>();
for (Target target : targets) {
if (visited.contains(target.pos)) {
continue;
}
List<Target> cluster = new ArrayList<>();
ArrayDeque<BlockPos> queue = new ArrayDeque<>();
queue.add(target.pos);
visited.add(target.pos);
while (!queue.isEmpty()) {
BlockPos current = queue.poll();
Target currentTarget = map.get(current);
if (currentTarget != null) {
cluster.add(currentTarget);
}
for (BlockPos neighbor : new BlockPos[]{
current.above(), current.below(),
current.north(), current.south(),
current.east(), current.west()
}) {
if (!visited.contains(neighbor) && map.containsKey(neighbor)) {
visited.add(neighbor);
queue.add(neighbor);
}
}
}
clusters.add(cluster);
}
return clusters;
}
private Vec3 clusterCenter(List<Target> cluster) {
double x = 0.0;
double y = 0.0;
double z = 0.0;
for (Target target : cluster) {
Vec3 center = Vec3.atCenterOf(target.pos);
x += center.x;
y += center.y;
z += center.z;
}
int size = cluster.size();
return new Vec3(x / size, y / size, z / size);
}
public static class Target {
BlockPos pos;
BlockState state;
AABB box;
boolean visible;
}
private enum RenderStatus {
PRESERVE,
HIDDEN,
VISIBLE
}
private record ObservedTarget(BlockState state, boolean rendered) {
}
}

View file

@ -0,0 +1,312 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.module.modules.visuals;
import dev.loki.lovisual.config.common.CommonSettingSchemas;
import dev.loki.lovisual.config.values.BooleanValue;
import dev.loki.lovisual.config.values.EnumValue;
import dev.loki.lovisual.config.values.NumberValue;
import dev.loki.lovisual.config.values.RGBAColorValue;
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.render.engine.animation.AnimatedRenderColors;
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 dev.loki.lovisual.render.helpers.HeadAnchor;
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
* {@link ChinaHatPalette}; this class only wires settings to rendering.
*/
@ModuleInfo(
id = "chinahat",
displayName = "ChinaHat",
category = ModuleCategory.VISUALS
)
public final class ChinaHat extends Module {
private final Minecraft mc = Minecraft.getInstance();
private final EnumValue<RenderTarget> renderTarget =
enumMode(
"render_target",
RenderTarget.BOTH,
RenderTarget.values()
);
private final BooleanValue useRelations =
boolCommon(
"use_relations",
"use_relations",
CommonSettingSchemas.PLAYER_USE_RELATIONS,
true
);
private final RGBAColorValue primaryColor =
visibleWhen(common(
color(
"primary_color",
"#CCE8C547"
),
CommonSettingSchemas.RENDER_PRIMARY_COLOR.commonI18nKey()
), () -> !useRelations.get());
private final EnumValue<ColorMode> colorMode =
enumCommon(
"color_mode",
"color_mode",
CommonSettingSchemas.RENDER_COLOR_MODE,
ColorMode.STATIC,
ColorMode.values()
);
private final RGBAColorValue secondaryColor =
visibleWhen(common(
color(
"secondary_color",
"#AA9A7A1A"
),
CommonSettingSchemas.RENDER_SECONDARY_COLOR.commonI18nKey()
), this::usesSecondaryManualColor);
private final NumberValue<Integer> colorSpeed =
numCommon(
"color_speed",
"color_speed",
CommonSettingSchemas.RENDER_COLOR_SPEED,
18,
2,
54
);
private final NumberValue<Float> scale =
numCommon(
"scale",
"scale",
CommonSettingSchemas.RENDER_SCALE,
1.15f,
0.45f,
2.0f
);
private final BooleanValue depthTest =
boolCommon(
"depth_test",
"depth_test",
CommonSettingSchemas.RENDER_DEPTH_TEST,
true
);
private final RGBAColorValue rimColor =
visibleWhen(color(
"rim_color",
"#EEF3D18A"
), () -> !useRelations.get());
private final NumberValue<Integer> segments =
num(
"segments",
40,
6,
96
);
private final NumberValue<Float> brimRadius =
num(
"brim_radius",
0.45f,
0.2f,
0.7f
);
private final NumberValue<Float> domeRadius =
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
);
private final NumberValue<Float> headTopOffset =
num(
"head_top_offset",
0.3f,
0.05f,
0.5f
);
private final NumberValue<Float> headForwardOffset =
num(
"head_forward_offset",
0.0f,
-0.25f,
0.25f
);
private final NumberValue<Float> headRightOffset =
num(
"head_right_offset",
0.0f,
-0.25f,
0.25f
);
@Override
public WorldPhase getWorldPhase() {
return WorldPhase.BEFORE_TRANSLUCENT;
}
@Override
public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) {
if (!isEnabled() || mc.level == null || mc.player == null) return;
MeshBuilder tris = renderer.batch(
depthTest.get()
? LoVisualRenderPipelines.WORLD_COLORED_DEPTH
: LoVisualRenderPipelines.WORLD_COLORED,
depthTest.get()
? Renderer3D.DepthMode.PRE_DEPTH
: Renderer3D.DepthMode.NONE
);
MeshBuilder lines = renderer.batch(
depthTest.get()
? LoVisualRenderPipelines.WORLD_COLORED_LINES_DEPTH
: LoVisualRenderPipelines.WORLD_COLORED_LINES,
depthTest.get()
? Renderer3D.DepthMode.PRE_DEPTH
: Renderer3D.DepthMode.NONE
);
if (tris == null || lines == null) return;
if (shouldRenderSelf() && !mc.options.getCameraType().isFirstPerson()) {
renderAttached(tris, lines, mc.player, tickDelta);
}
if (shouldRenderPlayers()) {
for (Player player : mc.level.players()) {
if (player == null || player == mc.player || player.isRemoved()) continue;
if (!player.isAlive() || player.getHealth() <= 0.0f) continue;
renderAttached(tris, lines, player, tickDelta);
}
}
}
private boolean shouldRenderSelf() {
return renderTarget.get() == RenderTarget.SELF || renderTarget.get() == RenderTarget.BOTH;
}
private boolean shouldRenderPlayers() {
return renderTarget.get() == RenderTarget.PLAYERS || renderTarget.get() == RenderTarget.BOTH;
}
private boolean usesSecondaryManualColor() {
if (useRelations.get()) return false;
return AnimatedRenderColors.usesSecondary(animatedColorMode());
}
private void renderAttached(MeshBuilder tris, MeshBuilder lines, Player player, float tickDelta) {
HeadAnchor anchor = HeadAnchor.of(
player,
tickDelta,
headTopOffset.get(),
headForwardOffset.get(),
headRightOffset.get()
);
ChinaHatPalette.Palette palette = ChinaHatPalette.resolve(
player,
useRelations.get(),
primaryColor.getArgb(),
secondaryColor.getArgb(),
rimColor.getArgb()
);
AnimatedRenderColors.Mode mode = animatedColorMode();
int speed = colorSpeed.get();
float size = scale.get();
ChinaHatModel.build(
tris,
lines,
anchor,
segments.get(),
brimRadius.get() * size,
domeRadius.get() * size,
domeHeight.get() * size,
brimThickness.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
);
}
private AnimatedRenderColors.Mode animatedColorMode() {
return switch (colorMode.get()) {
case RAINBOW -> AnimatedRenderColors.Mode.RAINBOW;
case LIGHT_RAINBOW -> AnimatedRenderColors.Mode.LIGHT_RAINBOW;
case SKY -> AnimatedRenderColors.Mode.SKY;
case FADE -> AnimatedRenderColors.Mode.FADE;
case DOUBLE_COLOR -> AnimatedRenderColors.Mode.DOUBLE_COLOR;
case ANALOGOUS -> AnimatedRenderColors.Mode.ANALOGOUS;
case THEME -> AnimatedRenderColors.Mode.THEME;
case STATIC -> AnimatedRenderColors.Mode.STATIC;
};
}
public enum RenderTarget implements EnumValue.IdProvider {
SELF("self"),
PLAYERS("players"),
BOTH("both");
private final String id;
RenderTarget(String id) {
this.id = id;
}
@Override
public String id() {
return id;
}
}
public enum ColorMode implements EnumValue.IdProvider {
STATIC("static"),
RAINBOW("rainbow"),
LIGHT_RAINBOW("light_rainbow"),
SKY("sky"),
FADE("fade"),
DOUBLE_COLOR("double_color"),
ANALOGOUS("analogous"),
THEME("theme");
private final String id;
ColorMode(String id) {
this.id = id;
}
@Override
public String id() {
return id;
}
}
}

View file

@ -0,0 +1,133 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.module.modules.visuals;
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
import dev.loki.lovisual.render.helpers.HeadAnchor;
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.
*/
final class ChinaHatModel {
private ChinaHatModel() {
}
interface Shade {
int color(int index, float verticalLight, float alphaMul);
}
static void build(MeshBuilder tris,
MeshBuilder lines,
HeadAnchor anchor,
int segments,
float brimRadius,
float domeRadius,
float domeHeight,
float brimThickness,
float yawDeg,
float pitchDeg,
float rollDeg,
Shade domeShade,
Shade brimShade,
Shade rimShade,
float alphaMul) {
if (tris == null || lines == 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);
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 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);
}
}
private static void triangle(MeshBuilder mesh, Vec3 a, Vec3 b, int cA, Vec3 c, int cC) {
mesh.ensureTriCapacity();
int i1 = putVertex(mesh, a, cA);
int i2 = putVertex(mesh, b, cA);
int i3 = putVertex(mesh, c, cC);
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);
int i2 = putVertex(mesh, b, cB);
mesh.line(i1, i2);
}
private static int putVertex(MeshBuilder mesh, Vec3 p, int argb) {
return mesh.vec3(p.x, p.y, p.z)
.color((argb >>> 24) & 255, (argb >>> 16) & 255, (argb >>> 8) & 255, argb & 255)
.next();
}
}

View file

@ -0,0 +1,144 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.module.modules.visuals;
import dev.loki.lovisual.features.relations.CategoryService;
import dev.loki.lovisual.render.engine.animation.AnimatedRenderColors;
import dev.loki.lovisual.render.engine.animation.AnimationUtility;
import dev.loki.lovisual.util.time.GameClock;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.player.Player;
/**
* Color resolution helpers for ChinaHat. Shared color-math and per-vertex shading,
* mirroring the TazikHat palette pipeline but reusable by the facade module.
*/
final class ChinaHatPalette {
private ChinaHatPalette() {
}
private static final double RAINBOW_SPATIAL_SCALE = 12.0;
record Palette(int primary, int secondary, int rim) {
}
static Palette resolve(Player player,
boolean useRelations,
int primaryArgb,
int secondaryArgb,
int rimArgb) {
if (useRelations) {
int relation = CategoryService.getColor(player);
int primary = withAlpha(relation, alphaOr(primaryArgb, 255));
int secondary = withAlpha(
AnimatedRenderColors.analogousColor(relation),
alphaOr(secondaryArgb, 170)
);
int rim = withAlpha(brighten(relation, 1.35f), alphaOr(rimArgb, 238));
return new Palette(primary, secondary, rim);
}
return new Palette(primaryArgb, secondaryArgb, rimArgb);
}
static int colorAt(Palette palette,
AnimatedRenderColors.Mode mode,
int speed,
int index,
float verticalLight,
float alphaMul) {
int dynamic = getDynamicColor(palette.primary, palette.secondary, mode, speed, index);
float circumference = 0.5f + 0.5f * Mth.sin(index * 0.47f + GameClock.millis() * 0.0007f);
int mixed = multiplyRgb(dynamic, verticalLight + circumference * 0.16f);
return withAlpha(mixed, Math.round(alpha(mixed) * alphaMul));
}
static int rimColorAt(Palette palette,
AnimatedRenderColors.Mode mode,
int speed,
int index,
float alphaMul) {
int dynamic = getDynamicColor(palette.rim, palette.secondary, mode, speed, index + 120);
float t = 0.5f + 0.5f * Mth.sin(index * 0.71f + GameClock.millis() * 0.0011f);
int mixed = mix(dynamic, brighten(palette.primary, 1.35f), t);
return withAlpha(mixed, Math.round(alpha(mixed) * alphaMul));
}
static int getDynamicColor(int primary,
int secondary,
AnimatedRenderColors.Mode mode,
int speed,
int index) {
int spatialIndex = Math.floorMod((int) Math.round(index * RAINBOW_SPATIAL_SCALE), 3600);
return AnimatedRenderColors.resolve(mode, speed, spatialIndex, primary, secondary, true);
}
static int mix(int a, int b, float t) {
float clamped = AnimationUtility.clamp01(t);
int aa = alpha(a);
int ar = red(a);
int ag = green(a);
int ab = blue(a);
int ba = alpha(b);
int br = red(b);
int bg = green(b);
int bb = blue(b);
int ca = Math.round(AnimationUtility.lerp(aa, ba, clamped));
int cr = Math.round(AnimationUtility.lerp(ar, br, clamped));
int cg = Math.round(AnimationUtility.lerp(ag, bg, clamped));
int cb = Math.round(AnimationUtility.lerp(ab, bb, clamped));
return (ca << 24) | (cr << 16) | (cg << 8) | cb;
}
static int multiplyRgb(int argb, float factor) {
int a = alpha(argb);
int r = Mth.clamp(Math.round(red(argb) * factor), 0, 255);
int g = Mth.clamp(Math.round(green(argb) * factor), 0, 255);
int b = Mth.clamp(Math.round(blue(argb) * factor), 0, 255);
return (a << 24) | (r << 16) | (g << 8) | b;
}
static int brighten(int argb, float factor) {
return multiplyRgb(argb, factor);
}
static int withAlpha(int argb, int alpha) {
int a = Mth.clamp(alpha, 0, 255);
return (a << 24) | (argb & 0x00FFFFFF);
}
static int alphaOr(int argb, int fallback) {
int alpha = alpha(argb);
return alpha <= 0 ? Mth.clamp(fallback, 0, 255) : alpha;
}
static int red(int argb) {
return (argb >>> 16) & 255;
}
static int green(int argb) {
return (argb >>> 8) & 255;
}
static int blue(int argb) {
return argb & 255;
}
static int alpha(int argb) {
return (argb >>> 24) & 255;
}
}

View file

@ -0,0 +1,76 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.module.modules.visuals;
import dev.loki.lovisual.config.values.NumberValue;
import dev.loki.lovisual.config.values.RGBAColorValue;
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.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.effect.MobEffects;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.phys.AABB;
@ModuleInfo(
id = "invisesp",
displayName = "InvisESP",
aliases = {"invis"},
category = ModuleCategory.VISUALS,
description = "module.invisesp.description"
)
public final class InvisEsp extends Module {
private final Minecraft mc = Minecraft.getInstance();
private final RGBAColorValue outlineColor =
color("invisesp_color", "color", "#FF00E5FF");
private final NumberValue<Float> lineWidth =
num("invisesp_line_width", "line_width", 1.5f, 0.5f, 6.0f);
{
setDefaultBind("L");
}
@Override
public WorldPhase getWorldPhase() {
return WorldPhase.END_MAIN;
}
@Override
public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) {
if (!isEnabled() || mc.level == null || mc.player == null) return;
float previousLineWidth = RenderState.lineWidth;
MeshBuilder lines;
try {
RenderState.lineWidth = lineWidth.get();
lines = renderer.batch(LoVisualRenderPipelines.WORLD_COLORED_LINES, Renderer3D.DepthMode.NONE);
} finally {
RenderState.lineWidth = previousLineWidth;
}
int color = outlineColor.getArgb();
for (Player player : mc.level.players()) {
if (player == mc.player) continue;
if (!isInvisible(player)) continue;
AABB box = Esp2DRenderer.resolveWorldBox(player, tickDelta);
EspMeshBuilder.addOutlineBox(lines, box, color, color, color, color, color, color, color, color);
}
}
private boolean isInvisible(Player player) {
return player.isInvisible() || player.hasEffect(MobEffects.INVISIBILITY);
}
}

View file

@ -1,45 +0,0 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.module.modules.visuals;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
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;
//todo Description
@ModuleInfo(
id = "seeinvisibles",
displayName = "SeeInvisibles",
category = ModuleCategory.VISUALS
)
public class SeeInvisibles extends Module {
private final NumberValue<Integer> alpha = num("seeInvisiblesAlpha", "alpha", 110, 0, 255);
public boolean shouldRenderInvisiblePlayer(LivingEntity entity) {
return isEnabled() && entity instanceof Player && entity.isInvisible();
}
public boolean shouldRenderInvisiblePlayer(EntityType<?> entityType, boolean invisible) {
return isEnabled() && invisible && entityType == net.minecraft.world.entity.EntityTypes.PLAYER;
}
public int getInvisiblePlayerTintArgb() {
int alphaByte = Mth.clamp(alpha.get(), 0, 255);
return (alphaByte << 24) | 0x00FFFFFF;
}
public float getInvisiblePlayerAlpha01() {
return Mth.clamp(alpha.get(), 0, 255) / 255.0f;
}
}

View file

@ -1,356 +0,0 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.module.modules.visuals;
import com.mojang.blaze3d.textures.GpuSampler;
import com.mojang.blaze3d.textures.GpuTextureView;
import dev.loki.lovisual.features.module.*;
import dev.loki.lovisual.features.module.Module;
import dev.loki.lovisual.util.screen.ClientScreen;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.inventory.InventoryScreen;
import net.minecraft.client.renderer.texture.AbstractTexture;
import net.minecraft.resources.Identifier;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.phys.Vec2;
import net.minecraft.world.phys.Vec3;
import org.joml.Matrix4f;
import dev.loki.lovisual.config.values.BooleanValue;
import dev.loki.lovisual.config.values.ModeValue;
import dev.loki.lovisual.features.module.*;
import dev.loki.lovisual.features.relations.CategoryService;
import dev.loki.lovisual.mixininterface.IEntity;
import dev.loki.lovisual.render.engine.animation.AnimationUtility;
import dev.loki.lovisual.render.engine.color.RenderColor;
import dev.loki.lovisual.render.engine.core.ViewportContext;
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
import dev.loki.lovisual.render.engine.renderer.MeshRenderer;
import dev.loki.lovisual.render.engine.renderer.Renderer2D;
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
import dev.loki.lovisual.render.engine.text.TextRenderer;
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
import java.util.List;
//todo Description
@ModuleInfo(id = "tracers", displayName = "Tracers", aliases = {"arrows", "lines"}, category = ModuleCategory.VISUALS)
public class Tracers extends Module {
private static final String SETTING_TRACE_FRIENDS = "trace_friends";
private static final String SETTING_MOVE_RADIUS_BONUS = "move_radius_bonus";
private static final String SETTING_MODE = "mode";
private static final String SETTING_ARROW_TEXTURE = "arrow_texture";
private static final Identifier ARROW_1 = Identifier.fromNamespaceAndPath("lovisual", "textures/hud/elements/arrow.png");
private static final Identifier ARROW_2 = Identifier.fromNamespaceAndPath("lovisual", "textures/hud/elements/arrow2.png");
private static final Identifier ARROW_3 = Identifier.fromNamespaceAndPath("lovisual", "textures/hud/elements/arrow3.png");
private static final Identifier ARROW_4 = Identifier.fromNamespaceAndPath("lovisual", "textures/hud/elements/arrow4.png");
private static final float BASE_RADIUS = 48.0f;
private static final float INVENTORY_RADIUS_BONUS = 50.0f;
private static final float MOVE_RADIUS_BONUS = 1.5f;
private static final float ARROW_W = 10.0f;
private static final float ARROW_H = 10.0f;
private static final float ARROW_OFFSET_X = -5.0f;
private static final float ARROW_OFFSET_Y = -6.0f;
private static final int ARROW_ALPHA = 235;
private static final float ARROW_ROTATION_OFFSET_DEG = 90.0f;
private static final float ARROW_DISTANCE_NEAR = 6.0f;
private static final float ARROW_DISTANCE_FAR = 60.0f;
private static final float ARROW_RADIUS_MIN_FACTOR = 0.55f;
private static final float ARROW_RADIUS_MAX_FACTOR = 1.0f;
private static final MeshBuilder ARROW_MESH =
new MeshBuilder(LoVisualRenderPipelines.UI_TEXTURED_ADDITIVE);
private final Minecraft mc = Minecraft.getInstance();
private final BooleanValue traceFriendsValue = bool("tracersTraceFriends", SETTING_TRACE_FRIENDS, false);
private final ModeValue tracersMode =
modeSetting("tracersMode", SETTING_MODE, "Lines", "Lines", "Arrows");
private final BooleanValue moveRadiusBonusValue =
visibleWhen(bool("tracersMoveRadiusBonus", SETTING_MOVE_RADIUS_BONUS, false), this::isArrowsMode);
private final ModeValue arrowTexture =
visibleWhen(modeSetting("tracersArrowTexture", SETTING_ARROW_TEXTURE, "Arrow", "Arrow", "Arrow2", "Arrow3", "Arrow4"),
this::isArrowsMode);
private float animationStep;
private float animatedYaw;
private float animatedPitch;
private float yaw;
private static boolean isMoving(Vec2 move) {
if (move == null) return false;
return move.x != 0.0f || move.y != 0.0f;
}
private static boolean isNameValid(String name) {
if (name == null) return false;
int len = name.length();
if (len < 3 || len > 16) return false;
for (int i = 0; i < len; i++) {
char c = name.charAt(i);
if (c == '_') continue;
if (!Character.isLetterOrDigit(c)) return false;
}
return true;
}
private static int withAlpha(int argb, int alpha) {
int a = Mth.clamp(alpha, 0, 255);
return (argb & 0x00FFFFFF) | (a << 24);
}
private static float radiusFactorForDistance(double distance) {
if (distance <= ARROW_DISTANCE_NEAR) return ARROW_RADIUS_MIN_FACTOR;
if (distance >= ARROW_DISTANCE_FAR) return ARROW_RADIUS_MAX_FACTOR;
float t = (float) ((distance - ARROW_DISTANCE_NEAR) / (ARROW_DISTANCE_FAR - ARROW_DISTANCE_NEAR));
t = Mth.clamp(t, 0.0f, 1.0f);
return Mth.lerp(t, ARROW_RADIUS_MIN_FACTOR, ARROW_RADIUS_MAX_FACTOR);
}
private static void renderArrow(Identifier textureId,
double x,
double y,
float angleRad,
int argb,
float scale) {
if (textureId == null) return;
Renderer2D.deferRenderThreadAction(() -> renderArrowImmediate(textureId, x, y, angleRad, argb, scale));
}
private static void renderArrowImmediate(Identifier textureId,
double x,
double y,
float angleRad,
int argb,
float scale) {
Minecraft mc = Minecraft.getInstance();
if (mc == null || mc.gameRenderer == null || mc.gameRenderer.mainRenderTarget() == null) return;
AbstractTexture tex = mc.getTextureManager().getTexture(textureId);
if (tex == null) return;
GpuTextureView view = tex.getTextureView();
GpuSampler sampler = tex.getSampler();
if (view == null || sampler == null) return;
RenderColor c = new RenderColor(argb);
ARROW_MESH.begin();
ARROW_MESH.ensureQuadCapacity();
int i1 = ARROW_MESH.vec2(ARROW_OFFSET_X * scale, ARROW_OFFSET_Y * scale)
.vec2(0, 0).color(c.r, c.g, c.b, c.a).next();
int i2 = ARROW_MESH.vec2(ARROW_OFFSET_X * scale, (ARROW_OFFSET_Y + ARROW_H) * scale)
.vec2(0, 1).color(c.r, c.g, c.b, c.a).next();
int i3 = ARROW_MESH.vec2((ARROW_OFFSET_X + ARROW_W) * scale, (ARROW_OFFSET_Y + ARROW_H) * scale)
.vec2(1, 1).color(c.r, c.g, c.b, c.a).next();
int i4 = ARROW_MESH.vec2((ARROW_OFFSET_X + ARROW_W) * scale, ARROW_OFFSET_Y * scale)
.vec2(1, 0).color(c.r, c.g, c.b, c.a).next();
ARROW_MESH.quad(i1, i2, i3, i4);
ARROW_MESH.end();
Matrix4f transform = new Matrix4f()
.translation((float) x, (float) y, 0.0f)
.rotateZ(angleRad);
MeshRenderer.begin()
.attachments(mc.gameRenderer.mainRenderTarget())
.pipeline(LoVisualRenderPipelines.UI_TEXTURED_ADDITIVE)
.mesh(ARROW_MESH)
.transform(transform)
.sampler("u_Texture", view, sampler)
.end();
}
@Override
public WorldPhase getWorldPhase() {
return WorldPhase.END_MAIN;
}
@Override
public HudPhase getHudPhase() {
return HudPhase.BEFORE_MISC_OVERLAYS;
}
@Override
public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) {
if (!isEnabled() || mc.level == null || mc.player == null) return;
if (!isLinesMode()) return;
Vec3 camPos = getActiveCameraPos(tickDelta);
Vec3 look = getActiveCameraLook(tickDelta);
Vec3 start = camPos.add(look.scale(10.0));
for (Player p : mc.level.players()) {
if (p == mc.player) continue;
if (!traceFriendsValue.get() && CategoryService.isFriend(p)) continue;
int c = resolveColor(p);
Vec3 targetWorld = obtainEntityLerpedPos(p, tickDelta);
renderer.line(
start.x, start.y, start.z,
targetWorld.x, targetWorld.y, targetWorld.z,
(c >> 16) & 255,
(c >> 8) & 255,
c & 255,
(c >> 24) & 255
);
}
}
@Override
public void onRenderHudEngineForeground(Renderer2D renderer, TextRenderer textRenderer, net.minecraft.client.gui.GuiGraphicsExtractor ctx, float tickDelta) {
if (!isEnabled() || mc.level == null || mc.player == null) return;
if (!isArrowsMode()) return;
if (!mc.options.getCameraType().isFirstPerson()) return;
Vec2 move = mc.player.input != null ? mc.player.input.getMoveVector() : Vec2.ZERO;
float moveX = move.x;
float moveZ = move.y;
float scale = ViewportContext.getScaleFactor();
float moveScale = moveRadiusBonusValue.get() ? 10.0f : 0.0f;
animatedYaw = AnimationUtility.fast(animatedYaw, moveX * moveScale * scale, 5.0f);
animatedPitch = AnimationUtility.fast(animatedPitch, moveZ * moveScale * scale, 5.0f);
yaw = AnimationUtility.fast(yaw, getActiveCameraYaw(), 10.0f);
float size = BASE_RADIUS;
if (ClientScreen.current() instanceof InventoryScreen) {
size += INVENTORY_RADIUS_BONUS;
}
if (moveRadiusBonusValue.get() && isMoving(move)) {
size += MOVE_RADIUS_BONUS;
}
animationStep = AnimationUtility.fast(animationStep, size * scale, 6.0f);
double centerX = mc.getWindow().getWidth() * 0.5;
double centerY = mc.getWindow().getHeight() * 0.5;
Vec3 camPos = getActiveCameraPos(tickDelta);
Identifier textureId = resolveArrowTexture();
for (Player player : mc.level.players()) {
if (player == mc.player) continue;
if (!traceFriendsValue.get() && CategoryService.isFriend(player)) continue;
Vec3 targetWorld = obtainEntityLerpedPos(player, tickDelta);
double relX = targetWorld.x - camPos.x;
double relZ = targetWorld.z - camPos.z;
double yawRad = Math.toRadians(yaw);
double cos = Math.cos(yawRad);
double sin = Math.sin(yawRad);
double rotY = -(relZ * cos - relX * sin);
double rotX = -(relX * cos + relZ * sin);
float angleDeg = (float) Math.toDegrees(Math.atan2(rotY, rotX));
double dist = targetWorld.distanceTo(camPos);
float radiusFactor = shouldUseAdaptiveRadius() ? radiusFactorForDistance(dist) : 1.0f;
double radius = animationStep * radiusFactor;
double x2 = radius * Mth.cos((float) Math.toRadians(angleDeg)) + centerX + animatedYaw;
double y2 = radius * Mth.sin((float) Math.toRadians(angleDeg)) + centerY + animatedPitch;
int color = withAlpha(resolveColor(player), ARROW_ALPHA);
float rotRad = (float) Math.toRadians(angleDeg + ARROW_ROTATION_OFFSET_DEG);
renderArrow(textureId, x2, y2, rotRad, color, scale);
}
}
private Vec3 getActiveCameraPos(float tickDelta) {
Freecam fc = Modules.get(Freecam.class);
if (fc != null && fc.isEnabled()) {
return new Vec3(
Mth.lerp(tickDelta, fc.camPosPrev.x, fc.camPos.x),
Mth.lerp(tickDelta, fc.camPosPrev.y, fc.camPos.y),
Mth.lerp(tickDelta, fc.camPosPrev.z, fc.camPos.z)
);
}
return mc.gameRenderer.mainCamera().position();
}
private Vec3 getActiveCameraLook(float tickDelta) {
Freecam fc = Modules.get(Freecam.class);
if (fc != null && fc.isEnabled()) {
float yaw = (float) Math.toRadians(fc.camYaw);
float pitch = (float) Math.toRadians(fc.camPitch);
return new Vec3(
-Math.sin(yaw) * Math.cos(pitch),
-Math.sin(pitch),
Math.cos(yaw) * Math.cos(pitch)
).normalize();
}
return mc.player.getViewVector(tickDelta).normalize();
}
private float getActiveCameraYaw() {
Freecam fc = Modules.get(Freecam.class);
if (fc != null && fc.isEnabled()) {
return fc.camYaw;
}
return mc.gameRenderer.mainCamera().yRot();
}
public void render3DTracersRaw(
Renderer3D renderer,
float tickDelta,
List<Vec3> targets,
int argb
) {
if (mc.player == null || mc.level == null || targets == null || targets.isEmpty()) return;
Vec3 camPos = getActiveCameraPos(tickDelta);
Vec3 look = getActiveCameraLook(tickDelta);
Vec3 start = camPos.add(look.scale(10.0));
int a = (argb >> 24) & 255;
int r = (argb >> 16) & 255;
int g = (argb >> 8) & 255;
int b = argb & 255;
for (Vec3 targetWorld : targets) {
renderer.line(
start.x, start.y, start.z,
targetWorld.x, targetWorld.y, targetWorld.z,
r, g, b, a
);
}
}
private Vec3 obtainEntityLerpedPos(Entity e, float f) {
try {
return e.getPosition(f);
} catch (Throwable t) {
if (e instanceof IEntity a) {
return a.get$InstantRenderPos().lerp(e.position(), f);
}
return e.position();
}
}
private int resolveColor(Player p) {
return CategoryService.getColor(p);
}
private boolean shouldUseAdaptiveRadius() {
if (mc.player == null) return true;
return !(ClientScreen.current() instanceof InventoryScreen);
}
private Identifier resolveArrowTexture() {
String mode = arrowTexture.get();
if ("Arrow2".equalsIgnoreCase(mode)) return ARROW_2;
if ("Arrow3".equalsIgnoreCase(mode)) return ARROW_3;
if ("Arrow4".equalsIgnoreCase(mode)) return ARROW_4;
return ARROW_1;
}
private boolean isLinesMode() {
return "Lines".equalsIgnoreCase(tracersMode.get());
}
private boolean isArrowsMode() {
return "Arrows".equalsIgnoreCase(tracersMode.get());
}
}

View file

@ -23,6 +23,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import dev.loki.lovisual.features.command.CommandManager; import dev.loki.lovisual.features.command.CommandManager;
import dev.loki.lovisual.features.gui.chat.BetterChatRenderer; import dev.loki.lovisual.features.gui.chat.BetterChatRenderer;
import dev.loki.lovisual.features.gui.chat.BetterChatTabHandler;
import dev.loki.lovisual.features.gui.hud.draggable.impl.BetterChat; import dev.loki.lovisual.features.gui.hud.draggable.impl.BetterChat;
import dev.loki.lovisual.mixins.accessors.ChatScreenSuggestorAccessor; import dev.loki.lovisual.mixins.accessors.ChatScreenSuggestorAccessor;
import dev.loki.lovisual.Lifecycle; import dev.loki.lovisual.Lifecycle;
@ -92,6 +93,15 @@ public class ChatScreenMixin {
} }
} }
@Inject(method = "keyPressed(Lnet/minecraft/client/input/KeyEvent;)Z", at = @At("HEAD"), cancellable = true, require = 0)
private void lovisual$betterChat$tabCompleteCommands(KeyEvent input, CallbackInfoReturnable<Boolean> cir) {
if (!Lifecycle.canRunHud()) return;
if (!BetterChat.isActive()) return;
if (BetterChatTabHandler.handleKey(input)) {
cir.setReturnValue(true);
}
}
@Unique @Unique
private boolean lovisual$betterChat$shouldHideVanillaChat() { private boolean lovisual$betterChat$shouldHideVanillaChat() {
if (!Lifecycle.canRunHud()) return false; if (!Lifecycle.canRunHud()) return false;

View file

@ -8,28 +8,19 @@
package dev.loki.lovisual.mixins; package dev.loki.lovisual.mixins;
import net.minecraft.client.multiplayer.ClientLevel; import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.core.BlockPos;
import net.minecraft.core.particles.ItemParticleOption; import net.minecraft.core.particles.ItemParticleOption;
import net.minecraft.core.particles.ParticleOptions; import net.minecraft.core.particles.ParticleOptions;
import net.minecraft.core.particles.ParticleTypes; import net.minecraft.core.particles.ParticleTypes;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import dev.loki.lovisual.features.module.Modules; import dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.visuals.NoRender; import dev.loki.lovisual.features.module.modules.visuals.NoRender;
import dev.loki.lovisual.util.block.BlockObservationHub;
@Mixin(ClientLevel.class) @Mixin(ClientLevel.class)
public abstract class ClientLevelMixin { public abstract class ClientLevelMixin {
@Inject(method = "setServerVerifiedBlockState", at = @At("TAIL"))
private void lovisual$recordBlockUpdate(BlockPos pos, BlockState state, @Block.UpdateFlags int flags, CallbackInfo ci) {
BlockObservationHub.observeWorldUpdate(pos, state);
}
@Inject( @Inject(
method = "doAddParticle(Lnet/minecraft/core/particles/ParticleOptions;ZZDDDDDD)V", method = "doAddParticle(Lnet/minecraft/core/particles/ParticleOptions;ZZDDDDDD)V",
at = @At("HEAD"), at = @At("HEAD"),

View file

@ -20,9 +20,6 @@ public abstract class LivingEntityRenderStateMixin implements ViewObstructionFad
@Unique @Unique
private float lovisual$viewObstructionFadeAlpha = 1.0f; private float lovisual$viewObstructionFadeAlpha = 1.0f;
@Unique
private boolean lovisual$seeInvisibleFadeActive;
@Override @Override
public boolean lovisual$isViewObstructionFadeActive() { public boolean lovisual$isViewObstructionFadeActive() {
return lovisual$viewObstructionFadeActive; return lovisual$viewObstructionFadeActive;
@ -42,14 +39,4 @@ public abstract class LivingEntityRenderStateMixin implements ViewObstructionFad
public void lovisual$setViewObstructionFadeAlpha(float alpha) { public void lovisual$setViewObstructionFadeAlpha(float alpha) {
this.lovisual$viewObstructionFadeAlpha = alpha; this.lovisual$viewObstructionFadeAlpha = alpha;
} }
@Override
public boolean lovisual$isSeeInvisibleFadeActive() {
return lovisual$seeInvisibleFadeActive;
}
@Override
public void lovisual$setSeeInvisibleFadeActive(boolean active) {
this.lovisual$seeInvisibleFadeActive = active;
}
} }

View file

@ -34,7 +34,6 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import dev.loki.lovisual.features.module.Modules; import dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.visuals.NameTags; import dev.loki.lovisual.features.module.modules.visuals.NameTags;
import dev.loki.lovisual.features.module.modules.visuals.NoRender; import dev.loki.lovisual.features.module.modules.visuals.NoRender;
import dev.loki.lovisual.features.module.modules.visuals.SeeInvisibles;
import dev.loki.lovisual.render.ViewObstructionFadeContext; import dev.loki.lovisual.render.ViewObstructionFadeContext;
import dev.loki.lovisual.render.ViewObstructionFadeState; import dev.loki.lovisual.render.ViewObstructionFadeState;
import dev.loki.lovisual.render.iris.IrisRuntime; import dev.loki.lovisual.render.iris.IrisRuntime;
@ -75,22 +74,10 @@ public abstract class LivingEntityRendererMixin<T extends LivingEntity, S extend
float obstructionFadeAlpha = obstructionFadeActive ? noRender.getEntityFadeAlpha(entity) : 1.0f; float obstructionFadeAlpha = obstructionFadeActive ? noRender.getEntityFadeAlpha(entity) : 1.0f;
obstructionFadeActive = obstructionFadeActive && obstructionFadeAlpha < 0.99f; obstructionFadeActive = obstructionFadeActive && obstructionFadeAlpha < 0.99f;
SeeInvisibles seeInvisibles = Modules.get(SeeInvisibles.class); boolean active = obstructionFadeActive;
boolean seeInvisibleActive = seeInvisibles != null && seeInvisibles.shouldRenderInvisiblePlayer(entity);
float seeInvisibleAlpha = seeInvisibleActive ? seeInvisibles.getInvisiblePlayerAlpha01() : 1.0f;
boolean seeInvisibleFadeActive = seeInvisibleActive && seeInvisibleAlpha < 0.99f;
if (seeInvisibleActive) {
state.isInvisibleToPlayer = false;
}
boolean active = obstructionFadeActive || seeInvisibleFadeActive;
float alpha = obstructionFadeAlpha; float alpha = obstructionFadeAlpha;
if (seeInvisibleFadeActive) {
alpha = Math.min(alpha, seeInvisibleAlpha);
}
fadeState.lovisual$setViewObstructionFadeActive(active); fadeState.lovisual$setViewObstructionFadeActive(active);
fadeState.lovisual$setViewObstructionFadeAlpha(active ? alpha : 1.0f); fadeState.lovisual$setViewObstructionFadeAlpha(active ? alpha : 1.0f);
fadeState.lovisual$setSeeInvisibleFadeActive(seeInvisibleFadeActive);
} }
@ModifyExpressionValue( @ModifyExpressionValue(
@ -169,21 +156,6 @@ public abstract class LivingEntityRendererMixin<T extends LivingEntity, S extend
return RenderTypes.entityTranslucent(getTextureLocation(state), true); return RenderTypes.entityTranslucent(getTextureLocation(state), true);
} }
@ModifyConstant(
method = "submit",
constant = @Constant(intValue = 654311423)
)
private int lovisual$seeInvisibles$overrideInvisiblePlayerAlpha(int vanillaTint, S state) {
SeeInvisibles seeInvisibles = Modules.get(SeeInvisibles.class);
if (seeInvisibles != null
&& state instanceof ViewObstructionFadeState fadeState
&& fadeState.lovisual$isSeeInvisibleFadeActive()
&& seeInvisibles.shouldRenderInvisiblePlayer(state.entityType, state.isInvisible)) {
return seeInvisibles.getInvisiblePlayerTintArgb();
}
return vanillaTint;
}
@Inject(method = "submit", at = @At("HEAD")) @Inject(method = "submit", at = @At("HEAD"))
private void lovisual$pushViewObstructionFadeContext(S state, com.mojang.blaze3d.vertex.PoseStack matrixStack, net.minecraft.client.renderer.SubmitNodeCollector orderedRenderCommandQueue, net.minecraft.client.renderer.state.level.CameraRenderState cameraRenderState, CallbackInfo ci) { private void lovisual$pushViewObstructionFadeContext(S state, com.mojang.blaze3d.vertex.PoseStack matrixStack, net.minecraft.client.renderer.SubmitNodeCollector orderedRenderCommandQueue, net.minecraft.client.renderer.state.level.CameraRenderState cameraRenderState, CallbackInfo ci) {
ViewObstructionFadeContext.push(state instanceof ViewObstructionFadeState fadeState ? fadeState : null); ViewObstructionFadeContext.push(state instanceof ViewObstructionFadeState fadeState ? fadeState : null);

View file

@ -1,64 +0,0 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins.sodium;
import net.caffeinemc.mods.sodium.client.render.chunk.terrain.material.Material;
import net.caffeinemc.mods.sodium.client.render.model.MutableQuadViewImpl;
import net.minecraft.client.renderer.block.dispatch.BlockStateModel;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.state.BlockState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Pseudo;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import dev.loki.lovisual.util.block.BlockObservationHub;
import java.util.ArrayDeque;
@Pseudo
@Mixin(targets = "net.caffeinemc.mods.sodium.client.render.chunk.compile.pipeline.BlockRenderer")
public abstract class SodiumBlockVisibilityMixin {
@Unique
private static final ThreadLocal<ArrayDeque<RenderedBlock>> LOVISUAL_BLOCK_STACK =
ThreadLocal.withInitial(ArrayDeque::new);
@Inject(method = "renderModel", at = @At("HEAD"), remap = false)
private void lovisual$pushRenderedBlock(BlockStateModel model, BlockState state, BlockPos pos, BlockPos origin, CallbackInfo ci) {
LOVISUAL_BLOCK_STACK.get().addLast(new RenderedBlock(pos, state));
}
@Inject(method = "renderModel", at = @At("RETURN"), remap = false)
private void lovisual$popRenderedBlock(BlockStateModel model, BlockState state, BlockPos pos, BlockPos origin, CallbackInfo ci) {
ArrayDeque<RenderedBlock> stack = LOVISUAL_BLOCK_STACK.get();
if (!stack.isEmpty()) {
stack.removeLast();
}
if (stack.isEmpty()) {
LOVISUAL_BLOCK_STACK.remove();
}
}
@Inject(method = "bufferQuad", at = @At("HEAD"), remap = false)
private void lovisual$observeBufferedQuad(MutableQuadViewImpl quad,
float[] brightness,
Material material,
CallbackInfo ci) {
ArrayDeque<RenderedBlock> stack = LOVISUAL_BLOCK_STACK.get();
if (stack.isEmpty()) {
return;
}
RenderedBlock rendered = stack.peekLast();
BlockObservationHub.observeSodiumRenderedBlock(rendered.pos(), rendered.state());
}
private record RenderedBlock(BlockPos pos, BlockState state) {
}
}

View file

@ -1,35 +0,0 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.mixins.sodium;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.world.level.block.state.BlockState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Pseudo;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyVariable;
import dev.loki.lovisual.util.block.BlockObservationHub;
@Pseudo
@Mixin(targets = "net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks.ChunkBuilderMeshingTask")
public abstract class SodiumChunkBuilderMeshingTaskMixin {
@ModifyVariable(
method = "execute(Lnet/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkBuildContext;Lnet/caffeinemc/mods/sodium/client/util/task/CancellationToken;)Lnet/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkBuildOutput;",
at = @At("STORE"),
ordinal = 0,
remap = false
)
private BlockState lovisual$captureSodiumSectionBlock(BlockState state,
@Local(index = 22) int x,
@Local(index = 20) int y,
@Local(index = 21) int z) {
BlockObservationHub.observeSodiumBlock(x, y, z, state);
return state;
}
}

View file

@ -15,8 +15,4 @@ public interface ViewObstructionFadeState {
void lovisual$setViewObstructionFadeActive(boolean active); void lovisual$setViewObstructionFadeActive(boolean active);
void lovisual$setViewObstructionFadeAlpha(float alpha); void lovisual$setViewObstructionFadeAlpha(float alpha);
boolean lovisual$isSeeInvisibleFadeActive();
void lovisual$setSeeInvisibleFadeActive(boolean active);
} }

View file

@ -1,177 +0,0 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.util.block;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Hot-path Sodium bridge for BlockESP.
*
* <p>This class is intentionally primitive-only on the meshing path: no module lookup,
* no setting reads, no BlockPos allocation unless a configured block actually matched.</p>
*/
public enum BlockEspSodiumCandidateCollector {
;
private static final int MAX_QUEUED_CANDIDATES = 32768;
private static final Snapshot DISABLED = new Snapshot(false, Set.of(), false, 0, 0, 0, 0, 0);
private static final ConcurrentLinkedQueue<Long> CANDIDATE_KEYS = new ConcurrentLinkedQueue<>();
private static final Map<Long, Candidate> PENDING_CANDIDATES = new ConcurrentHashMap<>();
private static final AtomicInteger QUEUED_COUNT = new AtomicInteger();
private static volatile Snapshot snapshot = DISABLED;
public static void publishSnapshot(
boolean enabled,
Set<Block> targetBlocks,
boolean limitDistance,
int maxDistance,
int centerX,
int centerY,
int centerZ,
int generation
) {
if (!enabled || targetBlocks == null || targetBlocks.isEmpty()) {
snapshot = DISABLED;
clear();
return;
}
snapshot = new Snapshot(
true,
Set.copyOf(targetBlocks),
limitDistance,
Math.max(1, maxDistance),
centerX,
centerY,
centerZ,
generation
);
}
public static void disable() {
snapshot = DISABLED;
clear();
}
public static void clear() {
CANDIDATE_KEYS.clear();
PENDING_CANDIDATES.clear();
QUEUED_COUNT.set(0);
}
public static void observeSodiumBlock(int x, int y, int z, BlockState state) {
offer(x, y, z, state, CandidateSource.SODIUM_SECTION_SCAN);
}
public static void observeSodiumRenderedBlock(int x, int y, int z, BlockState state) {
offer(x, y, z, state, CandidateSource.SODIUM_BUFFERED_QUAD);
}
private static void offer(int x, int y, int z, BlockState state, CandidateSource source) {
Snapshot current = snapshot;
if (!current.enabled || state == null || state.isAir()) {
return;
}
if (!current.targets.contains(state.getBlock())) {
return;
}
if (current.limitDistance && !current.withinDistance(x, y, z)) {
return;
}
long packedPos = BlockPos.asLong(x, y, z);
Candidate candidate = new Candidate(packedPos, state, source, current.generation);
while (true) {
Candidate pending = PENDING_CANDIDATES.putIfAbsent(packedPos, candidate);
if (pending == null) {
break;
}
if (source != CandidateSource.SODIUM_BUFFERED_QUAD
|| pending.source == CandidateSource.SODIUM_BUFFERED_QUAD) {
return;
}
if (PENDING_CANDIDATES.replace(packedPos, pending, candidate)) {
return;
}
}
int queued = QUEUED_COUNT.incrementAndGet();
if (queued > MAX_QUEUED_CANDIDATES) {
QUEUED_COUNT.decrementAndGet();
PENDING_CANDIDATES.remove(packedPos, candidate);
return;
}
CANDIDATE_KEYS.add(packedPos);
}
public static int drain(int maxCandidates, CandidateConsumer consumer) {
if (maxCandidates <= 0 || consumer == null) {
return 0;
}
int drained = 0;
while (drained < maxCandidates) {
Long packedPos = CANDIDATE_KEYS.poll();
if (packedPos == null) {
break;
}
Candidate candidate = PENDING_CANDIDATES.remove(packedPos);
if (candidate == null) {
continue;
}
QUEUED_COUNT.decrementAndGet();
consumer.accept(candidate);
drained++;
}
return drained;
}
public enum CandidateSource {
SODIUM_SECTION_SCAN,
SODIUM_BUFFERED_QUAD
}
@FunctionalInterface
public interface CandidateConsumer {
void accept(Candidate candidate);
}
private record Snapshot(
boolean enabled,
Set<Block> targets,
boolean limitDistance,
int maxDistance,
int centerX,
int centerY,
int centerZ,
int generation
) {
boolean withinDistance(int x, int y, int z) {
long dx = (long) x - centerX;
long dy = (long) y - centerY;
long dz = (long) z - centerZ;
long max = maxDistance;
return dx * dx + dy * dy + dz * dz <= max * max;
}
}
public record Candidate(long packedPos, BlockState state, CandidateSource source, int generation) {
}
}

View file

@ -1,103 +0,0 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.util.block;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.client.Minecraft;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.state.BlockState;
import dev.loki.lovisual.features.module.Modules;
import dev.loki.lovisual.features.module.modules.visuals.BedwarsESP;
import dev.loki.lovisual.features.module.modules.visuals.BlockESP;
import dev.loki.lovisual.util.block.bed.BedBlockUtil;
public enum BlockObservationHub {
;
public static void requestSodiumRebuild(Minecraft client, int requestedRadius) {
if (client == null || client.level == null || client.player == null) {
return;
}
if (!FabricLoader.getInstance().isModLoaded("sodium")) {
return;
}
try {
Class<?> cls = Class.forName("net.caffeinemc.mods.sodium.client.render.SodiumWorldRenderer");
Object renderer = cls.getMethod("instanceNullable").invoke(null);
if (renderer == null) {
return;
}
int renderRadius = (client.options.getEffectiveRenderDistance() + 1) * 16;
int radius = Math.max(1, Math.min(requestedRadius, renderRadius));
BlockPos center = client.player.blockPosition();
cls.getMethod(
"scheduleRebuildForBlockArea",
int.class, int.class, int.class, int.class, int.class, int.class, boolean.class
).invoke(
renderer,
center.getX() - radius,
client.level.getMinY(),
center.getZ() - radius,
center.getX() + radius,
client.level.getMaxY(),
center.getZ() + radius,
true
);
} catch (Throwable ignored) {
// Sodium отсутствует или изменился API — молча пропускаем
}
}
public static void observeSodiumBlock(int x, int y, int z, BlockState state) {
if (state == null || state.isAir()) {
return;
}
BlockEspSodiumCandidateCollector.observeSodiumBlock(x, y, z, state);
if (BedBlockUtil.isBed(state)) {
BedwarsESP bedwarsEsp = Modules.get(BedwarsESP.class);
if (bedwarsEsp != null && bedwarsEsp.isEnabled()) {
bedwarsEsp.acceptSodiumBlockState(x, y, z, state);
}
}
}
public static void observeSodiumRenderedBlock(BlockPos pos, BlockState state) {
if (pos == null || state == null || state.isAir()) {
return;
}
BlockEspSodiumCandidateCollector.observeSodiumRenderedBlock(pos.getX(), pos.getY(), pos.getZ(), state);
if (BedBlockUtil.isBed(state)) {
BedwarsESP bedwarsEsp = Modules.get(BedwarsESP.class);
if (bedwarsEsp != null && bedwarsEsp.isEnabled()) {
bedwarsEsp.acceptSodiumRenderedBlockState(pos, state);
}
}
}
public static void observeWorldUpdate(BlockPos pos, BlockState state) {
if (pos == null || state == null) {
return;
}
BlockESP blockEsp = Modules.get(BlockESP.class);
if (blockEsp != null && blockEsp.isEnabled()) {
blockEsp.acceptWorldBlockUpdate(pos, state);
}
BedwarsESP bedwarsEsp = Modules.get(BedwarsESP.class);
if (bedwarsEsp != null && bedwarsEsp.isEnabled()) {
bedwarsEsp.acceptWorldBlockUpdate(pos, state);
}
}
}

View file

@ -545,7 +545,6 @@
"setting.freecam.vertical_speed": "Vertical speed", "setting.freecam.vertical_speed": "Vertical speed",
"setting.freecam.toggle_input": "Toggle input", "setting.freecam.toggle_input": "Toggle input",
"setting.fullbright.min_light": "Min light", "setting.fullbright.min_light": "Min light",
"setting.seeinvisibles.alpha": "Alpha",
"setting.soundesp.name_mode": "Name mode", "setting.soundesp.name_mode": "Name mode",
"setting.soundesp.name_mode.option.Technical": "Technical", "setting.soundesp.name_mode.option.Technical": "Technical",
"setting.soundesp.name_mode.option.Localized": "Localized", "setting.soundesp.name_mode.option.Localized": "Localized",
@ -833,20 +832,6 @@
"setting.crosshair.orbiz_move_multiplier": "Move multiplier", "setting.crosshair.orbiz_move_multiplier": "Move multiplier",
"setting.crosshair.orbiz_motion_limit": "Motion limit", "setting.crosshair.orbiz_motion_limit": "Motion limit",
"setting.crosshair.orbiz_motion_smoothing": "Motion smoothing", "setting.crosshair.orbiz_motion_smoothing": "Motion smoothing",
"setting.tracers.trace_friends": "Trace friends",
"setting.tracers.move_radius_bonus": "Movement radius bonus",
"setting.tracers.mode": "Mode",
"setting.tracers.mode.option.Lines": "Lines",
"setting.tracers.mode.option.Arrows": "Arrows",
"setting.tracers.arrow_texture": "Arrow texture",
"setting.tracers.arrow_texture.option.Arrow": "Arrow 1",
"setting.tracers.arrow_texture.option.Arrow2": "Arrow 2",
"setting.tracers.arrow_texture.option.Arrow3": "Arrow 3",
"setting.tracers.arrow_texture.option.Arrow4": "Arrow 4",
"setting.tracers.default_color": "Default color",
"setting.tracers.friend_color": "Friend color",
"setting.tracers.enemy_color": "Enemy color",
"setting.tracers.staff_color": "Staff color",
"setting.trails.only_self": "Only self", "setting.trails.only_self": "Only self",
"setting.trails.hide_first_person": "Hide in first person", "setting.trails.hide_first_person": "Hide in first person",
"setting.trails.depth_test": "Depth test", "setting.trails.depth_test": "Depth test",
@ -1309,21 +1294,8 @@
"setting.blockhighlight.breaking_animation.option.static": "Static", "setting.blockhighlight.breaking_animation.option.static": "Static",
"setting.blockhighlight.breaking_color": "Breaking color", "setting.blockhighlight.breaking_color": "Breaking color",
"setting.blockhighlight.breaking_color2": "Second breaking color", "setting.blockhighlight.breaking_color2": "Second breaking color",
"setting.bedwarsesp.self_bed": "Self bed", "setting.invisesp.color": "Color",
"setting.bedwarsesp.self_bed.option.none": "None", "setting.invisesp.line_width": "Line width",
"setting.bedwarsesp.self_bed.option.color": "By color",
"setting.bedwarsesp.self_bed.option.spawn_location": "By spawn",
"setting.bedwarsesp.spawn_bed_distance": "Spawn bed distance",
"setting.bedwarsesp.max_layers": "Max layers",
"setting.bedwarsesp.show_openings": "Show openings",
"setting.bedwarsesp.self_bed_color": "Self bed color",
"setting.bedwarsesp.other_bed_color": "Other bed color",
"setting.bedwarsesp.self_opening_color": "Self opening color",
"setting.bedwarsesp.other_opening_color": "Other opening color",
"setting.blockesp.search_blocks": "Search blocks",
"setting.blockesp.visible_color": "Visible color",
"setting.blockesp.chunk_color": "Chunk color",
"setting.blockesp.cluster_tracers": "Cluster tracers",
"setting.holeesp.range_xz": "Range XZ", "setting.holeesp.range_xz": "Range XZ",
"setting.holeesp.range_y": "Range Y", "setting.holeesp.range_y": "Range Y",
"setting.holeesp.height": "Height", "setting.holeesp.height": "Height",
@ -1938,6 +1910,7 @@
"setting.draggable.triangulator.triangulator_cache_lifetime": "Cache lifetime", "setting.draggable.triangulator.triangulator_cache_lifetime": "Cache lifetime",
"setting.draggable.triangulator.triangulator_auto_reset": "Auto reset", "setting.draggable.triangulator.triangulator_auto_reset": "Auto reset",
"module.autobed.description": "Automatically places and detonates beds in explosive dimensions.", "module.autobed.description": "Automatically places and detonates beds in explosive dimensions.",
"module.invisesp.description": "Outlines players hidden by invisibility.",
"module.holeesp.description": "Highlights safe holes with a distance-faded fill.", "module.holeesp.description": "Highlights safe holes with a distance-faded fill.",
"setting.autobed.place": "Place", "setting.autobed.place": "Place",
"setting.autobed.explode": "Explode", "setting.autobed.explode": "Explode",
@ -3221,5 +3194,19 @@
"command.toggle.description": "Enables, disables, or toggles a module.", "command.toggle.description": "Enables, disables, or toggles a module.",
"command.tps.description": "Shows estimated server ticks per second.", "command.tps.description": "Shows estimated server ticks per second.",
"command.username.description": "Shows the current Minecraft username and UUID.", "command.username.description": "Shows the current Minecraft username and UUID.",
"command.xaero.description": "Adds, lists, or removes Xaero waypoints." "command.xaero.description": "Adds, lists, or removes Xaero waypoints.",
"module.chinahat.description": "Renders a classic conical Chinese hat on players' heads.",
"setting.chinahat.render_target": "Render",
"setting.chinahat.render_target.option.self": "Self",
"setting.chinahat.render_target.option.players": "Players",
"setting.chinahat.render_target.option.both": "Both",
"setting.chinahat.rim_color": "Rim color",
"setting.chinahat.segments": "Segments",
"setting.chinahat.brim_radius": "Brim radius",
"setting.chinahat.dome_radius": "Dome radius",
"setting.chinahat.dome_height": "Dome height",
"setting.chinahat.brim_thickness": "Brim thickness",
"setting.chinahat.head_top_offset": "Head height",
"setting.chinahat.head_forward_offset": "Forward offset",
"setting.chinahat.head_right_offset": "Right offset"
} }

View file

@ -480,7 +480,6 @@
"setting.freecam.vertical_speed": "Вертикальная скорость", "setting.freecam.vertical_speed": "Вертикальная скорость",
"setting.freecam.toggle_input": "Переключить ввод", "setting.freecam.toggle_input": "Переключить ввод",
"setting.fullbright.min_light": "Мин. свет", "setting.fullbright.min_light": "Мин. свет",
"setting.seeinvisibles.alpha": "Альфа",
"setting.soundesp.name_mode": "Режим имени", "setting.soundesp.name_mode": "Режим имени",
"setting.soundesp.name_mode.option.Technical": "Технический", "setting.soundesp.name_mode.option.Technical": "Технический",
"setting.soundesp.name_mode.option.Localized": "Локализованный", "setting.soundesp.name_mode.option.Localized": "Локализованный",
@ -768,20 +767,6 @@
"setting.crosshair.orbiz_move_multiplier": "Множитель движения", "setting.crosshair.orbiz_move_multiplier": "Множитель движения",
"setting.crosshair.orbiz_motion_limit": "Лимит движения", "setting.crosshair.orbiz_motion_limit": "Лимит движения",
"setting.crosshair.orbiz_motion_smoothing": "Сглаживание движения", "setting.crosshair.orbiz_motion_smoothing": "Сглаживание движения",
"setting.tracers.trace_friends": "Трейсить друзей",
"setting.tracers.move_radius_bonus": "Бонус радиуса от движения",
"setting.tracers.mode": "Режим",
"setting.tracers.mode.option.Lines": "Линии",
"setting.tracers.mode.option.Arrows": "Стрелки",
"setting.tracers.arrow_texture": "Текстура стрелки",
"setting.tracers.arrow_texture.option.Arrow": "Стрела 1",
"setting.tracers.arrow_texture.option.Arrow2": "Стрела 2",
"setting.tracers.arrow_texture.option.Arrow3": "Стрела 3",
"setting.tracers.arrow_texture.option.Arrow4": "Стрела 4",
"setting.tracers.default_color": "Цвет обычных",
"setting.tracers.friend_color": "Цвет друзей",
"setting.tracers.enemy_color": "Цвет врагов",
"setting.tracers.staff_color": "Цвет стаффа",
"setting.trails.only_self": "Только себя", "setting.trails.only_self": "Только себя",
"setting.trails.hide_first_person": "Скрывать в первом лице", "setting.trails.hide_first_person": "Скрывать в первом лице",
"setting.trails.depth_test": "Глубинный тест", "setting.trails.depth_test": "Глубинный тест",
@ -1103,22 +1088,9 @@
"setting.blockhighlight.breaking_animation.option.static": "Статичная", "setting.blockhighlight.breaking_animation.option.static": "Статичная",
"setting.blockhighlight.breaking_color": "Цвет ломания", "setting.blockhighlight.breaking_color": "Цвет ломания",
"setting.blockhighlight.breaking_color2": "Второй цвет ломания", "setting.blockhighlight.breaking_color2": "Второй цвет ломания",
"setting.bedwarsesp.self_bed": "Своя кровать",
"setting.bedwarsesp.self_bed.option.none": "Нет",
"setting.bedwarsesp.self_bed.option.color": "По цвету",
"setting.bedwarsesp.self_bed.option.spawn_location": "По спавну",
"setting.bedwarsesp.spawn_bed_distance": "Дистанция кровати от спавна",
"setting.bedwarsesp.max_layers": "Макс. слоёв",
"setting.bedwarsesp.show_openings": "Показывать вскрытия",
"setting.bedwarsesp.self_bed_color": "Цвет своей кровати",
"setting.bedwarsesp.other_bed_color": "Цвет чужой кровати",
"setting.bedwarsesp.self_opening_color": "Цвет вскрытия своей кровати",
"setting.bedwarsesp.other_opening_color": "Цвет вскрытия чужой кровати",
"setting.aspectratio.aspect_multiplier": "Множитель соотношения сторон", "setting.aspectratio.aspect_multiplier": "Множитель соотношения сторон",
"setting.blockesp.search_blocks": "Искомые блоки", "setting.invisesp.color": "Цвет",
"setting.blockesp.visible_color": "Цвет видимых", "setting.invisesp.line_width": "Толщина линии",
"setting.blockesp.chunk_color": "Цвет чанка",
"setting.blockesp.cluster_tracers": "Кластерные трейсеры",
"setting.holeesp.range_xz": "Радиус XZ", "setting.holeesp.range_xz": "Радиус XZ",
"setting.holeesp.range_y": "Радиус Y", "setting.holeesp.range_y": "Радиус Y",
"setting.holeesp.height": "Высота", "setting.holeesp.height": "Высота",
@ -1733,6 +1705,7 @@
"setting.draggable.triangulator.triangulator_cache_lifetime": "Время жизни кеша", "setting.draggable.triangulator.triangulator_cache_lifetime": "Время жизни кеша",
"setting.draggable.triangulator.triangulator_auto_reset": "Автосброс", "setting.draggable.triangulator.triangulator_auto_reset": "Автосброс",
"module.autobed.description": "Автоматически ставит и взрывает кровати в измерениях, где они взрываются.", "module.autobed.description": "Автоматически ставит и взрывает кровати в измерениях, где они взрываются.",
"module.invisesp.description": "Обводит игроков, скрытых невидимостью.",
"module.holeesp.description": "Подсвечивает безопасные холки с фейдом по дистанции.", "module.holeesp.description": "Подсвечивает безопасные холки с фейдом по дистанции.",
"setting.autobed.place": "Ставить", "setting.autobed.place": "Ставить",
"setting.autobed.explode": "Взрывать", "setting.autobed.explode": "Взрывать",
@ -3011,5 +2984,19 @@
"command.toggle.description": "Включает, отключает или переключает модуль.", "command.toggle.description": "Включает, отключает или переключает модуль.",
"command.tps.description": "Показывает расчётное количество серверных тиков в секунду.", "command.tps.description": "Показывает расчётное количество серверных тиков в секунду.",
"command.username.description": "Показывает текущее имя Minecraft и UUID.", "command.username.description": "Показывает текущее имя Minecraft и UUID.",
"command.xaero.description": "Добавляет, показывает или удаляет точки Xaero." "command.xaero.description": "Добавляет, показывает или удаляет точки Xaero.",
"module.chinahat.description": "Рисует классическую коническую китайскую шляпу на головах игроков.",
"setting.chinahat.render_target": "Рендер",
"setting.chinahat.render_target.option.self": "Себе",
"setting.chinahat.render_target.option.players": "Игрокам",
"setting.chinahat.render_target.option.both": "Всем",
"setting.chinahat.rim_color": "Цвет обода",
"setting.chinahat.segments": "Сегменты",
"setting.chinahat.brim_radius": "Радиус полей",
"setting.chinahat.dome_radius": "Радиус купола",
"setting.chinahat.dome_height": "Высота купола",
"setting.chinahat.brim_thickness": "Толщина полей",
"setting.chinahat.head_top_offset": "Высота на голове",
"setting.chinahat.head_forward_offset": "Смещение вперёд",
"setting.chinahat.head_right_offset": "Смещение вправо"
} }

View file

@ -171,9 +171,7 @@
"security.TranslatableTextContentMixin", "security.TranslatableTextContentMixin",
"security.TranslationStorageMixin", "security.TranslationStorageMixin",
"sodium.SodiumBlockRendererMixin", "sodium.SodiumBlockRendererMixin",
"sodium.SodiumBlockVisibilityMixin",
"sodium.SodiumChunkMeshFormatsMixin", "sodium.SodiumChunkMeshFormatsMixin",
"sodium.SodiumChunkBuilderMeshingTaskMixin",
"sodium.SodiumChunkVertexMixin", "sodium.SodiumChunkVertexMixin",
"sodium.SodiumShaderChunkRendererMixin", "sodium.SodiumShaderChunkRendererMixin",
"sodium.SodiumVertexConsumerTrackerMixin", "sodium.SodiumVertexConsumerTrackerMixin",

View file

@ -0,0 +1,89 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 pivosos2007.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.features.module.modules.visuals;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
final class ChinaHatPaletteTest {
@Test
void channelsAreIsolatedAndPreserved() {
int argb = 0xA1B2C3D4;
assertEquals(0xA1, ChinaHatPalette.alpha(argb));
assertEquals(0xB2, ChinaHatPalette.red(argb));
assertEquals(0xC3, ChinaHatPalette.green(argb));
assertEquals(0xD4, ChinaHatPalette.blue(argb));
}
@Test
void multiplyRgbKeepsAlphaAndClamps() {
int argb = 0xFF804020;
assertEquals(0xFFC06030, ChinaHatPalette.multiplyRgb(argb, 1.5f));
assertEquals(0xFF000000, ChinaHatPalette.multiplyRgb(argb, 0.0f));
assertEquals(0xFFFFFF80, ChinaHatPalette.multiplyRgb(argb, 4.0f));
}
@Test
void brightenUsesSameScalingAsMultiply() {
int argb = 0x80FF8000;
assertEquals(
ChinaHatPalette.multiplyRgb(argb, 1.25f),
ChinaHatPalette.brighten(argb, 1.25f)
);
}
@Test
void withAlphaReplacesOnlyTheAlphaChannel() {
int argb = 0xFF123456;
assertEquals(0x50123456, ChinaHatPalette.withAlpha(argb, 80));
assertEquals(0xFF123456, ChinaHatPalette.withAlpha(argb, 999));
assertEquals(0x00123456, ChinaHatPalette.withAlpha(argb, -5));
}
@Test
void alphaOrFallsBackOnlyWhenChannelIsZero() {
assertEquals(238, ChinaHatPalette.alphaOr(0x00123456, 238));
assertEquals(255, ChinaHatPalette.alphaOr(0xFF123456, 238));
}
@Test
void mixClampsAndBlendsEveryChannel() {
int a = 0xFF000000;
int b = 0x00FFFFFF;
assertEquals(a, ChinaHatPalette.mix(a, b, -1.0f));
assertEquals(b, ChinaHatPalette.mix(a, b, 2.0f));
assertEquals(0x80808080, ChinaHatPalette.mix(a, b, 0.5f));
}
@Test
void colorModeIdsAreUniqueAndLowercase() {
String[] ids = java.util.Arrays.stream(ChinaHat.ColorMode.values())
.map(ChinaHat.ColorMode::id)
.toArray(String[]::new);
assertEquals(ChinaHat.ColorMode.values().length, java.util.Arrays.stream(ids).distinct().count());
for (String id : ids) {
assertEquals(id, id.toLowerCase());
}
}
@Test
void renderTargetIdsAreUnique() {
String[] ids = java.util.Arrays.stream(ChinaHat.RenderTarget.values())
.map(ChinaHat.RenderTarget::id)
.toArray(String[]::new);
assertEquals(ChinaHat.RenderTarget.values().length, java.util.Arrays.stream(ids).distinct().count());
}
}